@stackbone/cli 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/main.js +383 -92
- package/package.json +1 -1
- package/runtime-deps/@stackbone/workflow-world/index.js +48 -13
- package/stackbone-cli-0.4.1.tgz +0 -0
- package/stackbone-cli-0.4.0.tgz +0 -0
package/main.js
CHANGED
|
@@ -1427,7 +1427,9 @@ var init_errors$1 = __esmMin((() => {
|
|
|
1427
1427
|
"connector_installation_required",
|
|
1428
1428
|
"principal_required",
|
|
1429
1429
|
"access_denied",
|
|
1430
|
-
"invalid_grant"
|
|
1430
|
+
"invalid_grant",
|
|
1431
|
+
"precondition_failed",
|
|
1432
|
+
"cardinality_mismatch"
|
|
1431
1433
|
];
|
|
1432
1434
|
BROKER_ERROR_CODES = z.enum([...CONNECTOR_EXECUTION_ERROR_CODES, ...BROKER_SPECIFIC_ERROR_CODES]);
|
|
1433
1435
|
z.object({
|
|
@@ -1460,6 +1462,19 @@ var init_errors$1 = __esmMin((() => {
|
|
|
1460
1462
|
connector_installation_required: false,
|
|
1461
1463
|
/** The refresh token is dead. Needs a fresh authorization, not another try. */
|
|
1462
1464
|
invalid_grant: false,
|
|
1465
|
+
/**
|
|
1466
|
+
* A write carrying a precondition matched no rows: somebody moved the row
|
|
1467
|
+
* between the read and the write, so the write was NOT applied (PLA-1403).
|
|
1468
|
+
*
|
|
1469
|
+
* Terminal, and this one is the whole point rather than a classification
|
|
1470
|
+
* detail. The condition is a fact about the provider's current state, so the
|
|
1471
|
+
* identical call refuses identically; and a retry is precisely the wrong move,
|
|
1472
|
+
* because the value the caller is guarding on is the stale one. What has to
|
|
1473
|
+
* happen next is a fresh read, which only the caller can do.
|
|
1474
|
+
*/
|
|
1475
|
+
precondition_failed: false,
|
|
1476
|
+
/** A narrower target is needed; repeating the same write exceeds the same limit. */
|
|
1477
|
+
cardinality_mismatch: false,
|
|
1463
1478
|
/** Transport, or the provider answering 5xx. */
|
|
1464
1479
|
execute_failed: true,
|
|
1465
1480
|
/** The deadline passed. The next attempt may fit inside it. */
|
|
@@ -1479,7 +1494,6 @@ var init_errors$1 = __esmMin((() => {
|
|
|
1479
1494
|
no_token: true,
|
|
1480
1495
|
user_authorization_required: true
|
|
1481
1496
|
};
|
|
1482
|
-
Object.entries(BROKER_ERROR_RETRYABILITY).filter(([, retryable]) => !retryable).map(([code]) => code);
|
|
1483
1497
|
REASON_ALIASES = { app_not_installed: "connector_installation_required" };
|
|
1484
1498
|
}));
|
|
1485
1499
|
//#endregion
|
|
@@ -1969,7 +1983,19 @@ var init_connect_surface = __esmMin((() => {
|
|
|
1969
1983
|
error: z.object({
|
|
1970
1984
|
/** The broker taxonomy code, e.g. `not_found`, `no_token`, `execute_failed`. */
|
|
1971
1985
|
code: z.string(),
|
|
1972
|
-
message: z.string()
|
|
1986
|
+
message: z.string(),
|
|
1987
|
+
/**
|
|
1988
|
+
* The broker's own retry classification, decided one code at a time and
|
|
1989
|
+
* overridable per site. It has to cross the wire (PLA-1349): the workflow
|
|
1990
|
+
* engine re-runs a failed step unless the error is fatal, and the SDK cannot
|
|
1991
|
+
* mark it fatal from a code alone without keeping a second copy of the
|
|
1992
|
+
* taxonomy that would drift from the broker's.
|
|
1993
|
+
*
|
|
1994
|
+
* Optional because an older box may not send it, and an absent value means
|
|
1995
|
+
* "not classified" rather than "retryable" — the SDK leaves such a failure
|
|
1996
|
+
* retryable, which is the safe direction for a transient outage.
|
|
1997
|
+
*/
|
|
1998
|
+
retryable: z.boolean().optional()
|
|
1973
1999
|
})
|
|
1974
2000
|
});
|
|
1975
2001
|
connectEvictResponseSchema = z.object({
|
|
@@ -9694,14 +9720,17 @@ var init_connector_invoker = __esmMin((() => {
|
|
|
9694
9720
|
* @param connector The connect connector id registered in Studio.
|
|
9695
9721
|
* @param operation The operation / tool id to run on that connector.
|
|
9696
9722
|
* @param args The operation arguments (path/query/body params). Defaults `{}`.
|
|
9697
|
-
* @param opts Optional `{ principal }`
|
|
9723
|
+
* @param opts Optional `{ principal, idempotent }`. `principal` defaults to
|
|
9724
|
+
* `{ type: 'app' }`; `idempotent` is UNSET by default and only
|
|
9725
|
+
* an explicit `false` changes how a failure is retried.
|
|
9698
9726
|
*/
|
|
9699
9727
|
async function callConnector(connector, operation, args, opts) {
|
|
9700
9728
|
return (await getConnectorInvoker().invoke({
|
|
9701
9729
|
connector,
|
|
9702
9730
|
operation,
|
|
9703
9731
|
...args !== void 0 ? { args } : {},
|
|
9704
|
-
...opts?.principal !== void 0 ? { principal: opts.principal } : {}
|
|
9732
|
+
...opts?.principal !== void 0 ? { principal: opts.principal } : {},
|
|
9733
|
+
...opts?.idempotent !== void 0 ? { idempotent: opts.idempotent } : {}
|
|
9705
9734
|
})).output;
|
|
9706
9735
|
}
|
|
9707
9736
|
var connectorHandle, connection;
|
|
@@ -15221,7 +15250,8 @@ var createTriggerSubscriptionsStore = (opts) => {
|
|
|
15221
15250
|
assertOpen();
|
|
15222
15251
|
await pool.query(`UPDATE stackbone_platform.trigger_deliveries
|
|
15223
15252
|
SET status = 'rejected', issues = $3::jsonb, updated_at = now()
|
|
15224
|
-
WHERE subscription_id = $1::uuid AND provider_item_id = $2
|
|
15253
|
+
WHERE subscription_id = $1::uuid AND provider_item_id = $2
|
|
15254
|
+
AND status IN ('claimed', 'starting')`, [
|
|
15225
15255
|
subscriptionId,
|
|
15226
15256
|
providerItemId,
|
|
15227
15257
|
JSON.stringify(issues)
|
|
@@ -15669,7 +15699,7 @@ var emulatorCors = (allowlist) => cors({
|
|
|
15669
15699
|
var STACKBONE_CLI_BUILD_VERSION, EMULATOR_CAPABILITY_ROUTES, EMULATOR_CAPABILITIES, buildContractResponse;
|
|
15670
15700
|
var init_contract = __esmMin((() => {
|
|
15671
15701
|
init_src$19();
|
|
15672
|
-
STACKBONE_CLI_BUILD_VERSION = "0.4.
|
|
15702
|
+
STACKBONE_CLI_BUILD_VERSION = "0.4.1";
|
|
15673
15703
|
EMULATOR_CAPABILITY_ROUTES = {
|
|
15674
15704
|
"database.postgres_direct": "/api/db",
|
|
15675
15705
|
"rag.basic": "/api/rag",
|
|
@@ -30692,6 +30722,8 @@ var init_types$1 = __esmMin((() => {
|
|
|
30692
30722
|
description: z.string().optional(),
|
|
30693
30723
|
parameters: z.array(operationParameterSchema).optional(),
|
|
30694
30724
|
requestBodySchema: z.unknown().optional(),
|
|
30725
|
+
/** Original Swagger body parameter name; allows body values alongside query filters. */
|
|
30726
|
+
bodyArgName: z.string().optional(),
|
|
30695
30727
|
responseSchema: z.unknown().optional(),
|
|
30696
30728
|
/**
|
|
30697
30729
|
* This operation's body is BYTES, not JSON. The executor then resolves the
|
|
@@ -31615,7 +31647,9 @@ var init_types = __esmMin((() => {
|
|
|
31615
31647
|
/** A policy denial on a VALID key: no credential eviction, no retry loop. */
|
|
31616
31648
|
accessDenied: z.array(z.string()).default([]),
|
|
31617
31649
|
/** A data conflict on a write (unique / FK violation): terminal, handleable. */
|
|
31618
|
-
conflict: z.array(z.string()).default([])
|
|
31650
|
+
conflict: z.array(z.string()).default([]),
|
|
31651
|
+
/** The provider rejected a write that exceeded its requested row limit. */
|
|
31652
|
+
cardinalityMismatch: z.array(z.string()).optional()
|
|
31619
31653
|
}).optional(),
|
|
31620
31654
|
/**
|
|
31621
31655
|
* The query parameter that carries "which columns to return" on this
|
|
@@ -33944,7 +33978,8 @@ var init_supabase_connector = __esmMin((() => {
|
|
|
33944
33978
|
"42"
|
|
33945
33979
|
],
|
|
33946
33980
|
accessDenied: ["42501"],
|
|
33947
|
-
conflict: ["23"]
|
|
33981
|
+
conflict: ["23"],
|
|
33982
|
+
cardinalityMismatch: ["PGRST124"]
|
|
33948
33983
|
},
|
|
33949
33984
|
scopes: [],
|
|
33950
33985
|
triggers: [{
|
|
@@ -34465,6 +34500,7 @@ function buildOperation(rawPath, resolvedPath, method, operation, pathLevelParam
|
|
|
34465
34500
|
const parameters = [];
|
|
34466
34501
|
const seen = /* @__PURE__ */ new Set();
|
|
34467
34502
|
let bodyParamSchema;
|
|
34503
|
+
let bodyArgName;
|
|
34468
34504
|
for (let i = rawParameters.length - 1; i >= 0; i--) {
|
|
34469
34505
|
const raw = derefParameter(rawParameters[i], root);
|
|
34470
34506
|
if (raw === void 0) {
|
|
@@ -34474,7 +34510,10 @@ function buildOperation(rawPath, resolvedPath, method, operation, pathLevelParam
|
|
|
34474
34510
|
if (isRecord$2(raw) && source === "swagger") {
|
|
34475
34511
|
const location = asString$1(raw["in"]);
|
|
34476
34512
|
if (location === "body") {
|
|
34477
|
-
if (bodyParamSchema === void 0 && raw["schema"] !== void 0)
|
|
34513
|
+
if (bodyParamSchema === void 0 && raw["schema"] !== void 0) {
|
|
34514
|
+
bodyParamSchema = raw["schema"];
|
|
34515
|
+
bodyArgName = asString$1(raw["name"]);
|
|
34516
|
+
}
|
|
34478
34517
|
continue;
|
|
34479
34518
|
}
|
|
34480
34519
|
if (location === "formData") {
|
|
@@ -34507,6 +34546,7 @@ function buildOperation(rawPath, resolvedPath, method, operation, pathLevelParam
|
|
|
34507
34546
|
...description !== void 0 ? { description } : {},
|
|
34508
34547
|
...parameters.length > 0 ? { parameters } : {},
|
|
34509
34548
|
...requestBodySchema !== void 0 ? { requestBodySchema } : {},
|
|
34549
|
+
...bodyArgName !== void 0 ? { bodyArgName } : {},
|
|
34510
34550
|
...responseSchema !== void 0 ? { responseSchema } : {},
|
|
34511
34551
|
...rawBody ? { rawBody } : {},
|
|
34512
34552
|
...rawResponse !== void 0 ? { rawResponse } : {},
|
|
@@ -36148,17 +36188,18 @@ function narrows(value, narrowingOperators) {
|
|
|
36148
36188
|
return !(operator === "is" && NEGATED_IS.has(rest.toLowerCase()));
|
|
36149
36189
|
}
|
|
36150
36190
|
/**
|
|
36151
|
-
* Should this call be refused as an unfiltered
|
|
36191
|
+
* Should this call be refused as an unfiltered DELETE or PATCH?
|
|
36152
36192
|
*
|
|
36153
36193
|
* Returns the refusal — `invalid_args`, not retryable, naming the columns that
|
|
36154
|
-
* would narrow it — or `null` when the call may proceed.
|
|
36155
|
-
*
|
|
36156
|
-
*
|
|
36157
|
-
* destroying data it never saw.
|
|
36194
|
+
* would narrow it — or `null` when the call may proceed. DELETE and PATCH
|
|
36195
|
+
* both affect existing rows. Reads and inserts keep their existing behavior.
|
|
36196
|
+
* Only declared query arguments count; values inside the named body do not.
|
|
36158
36197
|
*/
|
|
36159
36198
|
function unfilteredDeleteError(def, operation, args) {
|
|
36160
36199
|
const guard = def.unfilteredDeleteGuard;
|
|
36161
|
-
|
|
36200
|
+
const method = operation.method.toUpperCase();
|
|
36201
|
+
if (guard === void 0 || method !== "DELETE" && method !== "PATCH") return null;
|
|
36202
|
+
const effect = method === "PATCH" ? "update" : "remove";
|
|
36162
36203
|
const nonFilter = new Set(guard.nonFilterParams);
|
|
36163
36204
|
const filterParams = (operation.parameters ?? []).filter((param) => param.in === "query" && !nonFilter.has(param.name)).map((param) => param.name);
|
|
36164
36205
|
const present = filterParams.filter((name) => {
|
|
@@ -36169,9 +36210,9 @@ function unfilteredDeleteError(def, operation, args) {
|
|
|
36169
36210
|
if (narrowingOperators === void 0) {
|
|
36170
36211
|
if (present.length > 0) return null;
|
|
36171
36212
|
} else if (present.some((name) => narrows(args[name], narrowingOperators))) return null;
|
|
36172
|
-
if (narrowingOperators !== void 0 && present.length > 0) return refusal(operation, `no filter it carries narrows which rows go, so it would
|
|
36173
|
-
if (filterParams.length === 0) return refusal(operation,
|
|
36174
|
-
return refusal(operation,
|
|
36213
|
+
if (narrowingOperators !== void 0 && present.length > 0) return refusal(operation, `no filter it carries narrows which rows go, so it would ${effect} every row in the table.`, `Filters read: ${present.join(", ")}. Narrow it with one of: ${narrowingOperators.join(", ")}.`);
|
|
36214
|
+
if (filterParams.length === 0) return refusal(operation, `a ${method.toLowerCase()} with no row filter would ${effect} every row in the table.`, "The operation declares no filterable parameter, so nothing could narrow it.");
|
|
36215
|
+
return refusal(operation, `a ${method.toLowerCase()} with no row filter would ${effect} every row in the table.`, `Add at least one column filter: ${filterParams.join(", ")}.`);
|
|
36175
36216
|
}
|
|
36176
36217
|
/** The one refusal shape, so every path names the operation the same way. */
|
|
36177
36218
|
function refusal(operation, reason, remedy) {
|
|
@@ -36261,6 +36302,7 @@ function classifyProviderError(def, status, body, operationId) {
|
|
|
36261
36302
|
if (matched !== null && provider !== null) {
|
|
36262
36303
|
const said = providerSaid(provider);
|
|
36263
36304
|
switch (matched) {
|
|
36305
|
+
case "cardinalityMismatch": return makeBrokerError("cardinality_mismatch", `operation "${operationId}" exceeded the provider's requested row limit: ${said}`);
|
|
36264
36306
|
case "invalidArgs": return makeBrokerError("invalid_args", `operation "${operationId}" was refused by the provider as a bad request: ${said}`);
|
|
36265
36307
|
case "accessDenied": return makeBrokerError("access_denied", `operation "${operationId}" was denied by the provider's own policy — the credential is valid, the policy said no: ${said}`, false);
|
|
36266
36308
|
case "conflict": return makeBrokerError("execute_failed", `operation "${operationId}" hit a constraint conflict: ${said}`, false);
|
|
@@ -36295,7 +36337,8 @@ function longestMatch(classes, code) {
|
|
|
36295
36337
|
const lists = [
|
|
36296
36338
|
["invalidArgs", classes.invalidArgs],
|
|
36297
36339
|
["accessDenied", classes.accessDenied],
|
|
36298
|
-
["conflict", classes.conflict]
|
|
36340
|
+
["conflict", classes.conflict],
|
|
36341
|
+
["cardinalityMismatch", classes.cardinalityMismatch ?? []]
|
|
36299
36342
|
];
|
|
36300
36343
|
for (const [kind, prefixes] of lists) for (const prefix of prefixes) if (code.startsWith(prefix) && (best === null || prefix.length > best.prefix.length)) best = {
|
|
36301
36344
|
prefix,
|
|
@@ -36578,14 +36621,66 @@ async function runOperation(def, operation, args, token, http, timeoutMs, opts =
|
|
|
36578
36621
|
headerValue: responseHeader(response, continuation.urlHeader)
|
|
36579
36622
|
}) : void 0;
|
|
36580
36623
|
const output = response.ok && wantsRawResponse(operation, args) ? await storeRawResponse(operation, args, response, opts) : await parseResponse(response);
|
|
36624
|
+
const matchedRows = matchedRowTotal(response);
|
|
36581
36625
|
return {
|
|
36582
36626
|
status: response.status,
|
|
36583
36627
|
ok: response.ok,
|
|
36584
36628
|
output,
|
|
36629
|
+
...matchedRows !== void 0 ? { matchedRows } : {},
|
|
36585
36630
|
...continuationUrl !== void 0 ? { continuationUrl } : {}
|
|
36586
36631
|
};
|
|
36587
36632
|
}
|
|
36588
36633
|
/**
|
|
36634
|
+
* The TOTAL out of a `Content-Range` response header — how many rows the call
|
|
36635
|
+
* matched — or `undefined` when the provider did not count.
|
|
36636
|
+
*
|
|
36637
|
+
* `Content-Range` is `<first>-<last>/<total>`, and either half may be `*`. The
|
|
36638
|
+
* range says which rows came back in THIS response and the total says how many
|
|
36639
|
+
* exist; only the total answers "did my filter hit anything", because a write
|
|
36640
|
+
* that asks for no representation returns no rows either way. Measured against
|
|
36641
|
+
* PostgREST v12.2.3: a matched update answers `0-0/1`, one that matched nothing
|
|
36642
|
+
* answers a star range over a zero total, and one that asked for no count
|
|
36643
|
+
* answers a star over a star (`docs/investigations/pla-1345/README-live.md`).
|
|
36644
|
+
*
|
|
36645
|
+
* A `*` total is dropped rather than read as zero. That distinction is the guard
|
|
36646
|
+
* on the whole feature: see `matchedRows`.
|
|
36647
|
+
*/
|
|
36648
|
+
function matchedRowTotal(response) {
|
|
36649
|
+
const total = responseHeader(response, "Content-Range")?.split("/")[1];
|
|
36650
|
+
if (total === void 0 || total === "*") return void 0;
|
|
36651
|
+
const parsed = Number(total);
|
|
36652
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : void 0;
|
|
36653
|
+
}
|
|
36654
|
+
/**
|
|
36655
|
+
* Did a write carrying a precondition match nothing (PLA-1403)?
|
|
36656
|
+
*
|
|
36657
|
+
* Returns the terminal refusal, or `null` when the call may be reported as the
|
|
36658
|
+
* success it looks like. A conditional write sends the value it read as part of
|
|
36659
|
+
* the row filter, so a row somebody else moved matches nothing and is left
|
|
36660
|
+
* alone — the protection working. Without this the caller cannot tell, because
|
|
36661
|
+
* `execute` returns `{ ok: true, output }` and the count never leaves the
|
|
36662
|
+
* broker.
|
|
36663
|
+
*
|
|
36664
|
+
* TWO conditions, and both are load-bearing:
|
|
36665
|
+
*
|
|
36666
|
+
* * The method changes EXISTING rows: `PATCH` and `DELETE`, the same pair
|
|
36667
|
+
* `unfilteredDeleteError` guards. A `GET` that matches nothing is a correct
|
|
36668
|
+
* answer to a question and must stay a success — the one case this must
|
|
36669
|
+
* never break. `POST` is out because an insert matches nothing by
|
|
36670
|
+
* definition, and `PUT` is out because PostgREST upserts on it: a PUT that
|
|
36671
|
+
* matches nothing CREATES the row, so zero matched is not a refusal there.
|
|
36672
|
+
* Unmeasured on our side, which is the second reason to leave it out.
|
|
36673
|
+
* * The provider reported a total of exactly zero, which it only does when the
|
|
36674
|
+
* caller asked for the count. Asking IS the declaration, so a write that did
|
|
36675
|
+
* not ask keeps the behaviour it has always had. An empty body is NOT the
|
|
36676
|
+
* trigger.
|
|
36677
|
+
*/
|
|
36678
|
+
function unmatchedWriteError(operation, matchedRows, operationId) {
|
|
36679
|
+
const method = operation.method.toUpperCase();
|
|
36680
|
+
if (matchedRows !== 0 || method !== "PATCH" && method !== "DELETE") return null;
|
|
36681
|
+
return makeBrokerError("precondition_failed", `operation "${operationId}" matched no rows, so it changed nothing — the row moved since it was read. Re-read it and decide.`);
|
|
36682
|
+
}
|
|
36683
|
+
/**
|
|
36589
36684
|
* Render a parsed operation-call body to a short single-line string for an error
|
|
36590
36685
|
* message. Shared by the action executor (here) and the inbound-trigger poll
|
|
36591
36686
|
* context, so the two never drift. `empty` is returned for null/undefined (the
|
|
@@ -36599,13 +36694,36 @@ function formatOperationBody(output, opts = {}) {
|
|
|
36599
36694
|
return max > 0 && text.length > max ? `${text.slice(0, max)}…` : text;
|
|
36600
36695
|
}
|
|
36601
36696
|
/**
|
|
36697
|
+
* Fallback classification after provider-specific body codes (PLA-1321).
|
|
36698
|
+
* Known input, access and resource refusals are terminal. Unclassified statuses
|
|
36699
|
+
* retain the existing unknown-outcome policy, including 409: conflict vocabulary
|
|
36700
|
+
* is outside this slice. The caller's idempotency option still governs that path.
|
|
36701
|
+
*/
|
|
36702
|
+
function brokerCodeForProviderStatus(status) {
|
|
36703
|
+
switch (status) {
|
|
36704
|
+
case 400:
|
|
36705
|
+
case 405:
|
|
36706
|
+
case 422: return "invalid_args";
|
|
36707
|
+
case 403: return "access_denied";
|
|
36708
|
+
case 404:
|
|
36709
|
+
case 410: return "not_found";
|
|
36710
|
+
default: return "execute_failed";
|
|
36711
|
+
}
|
|
36712
|
+
}
|
|
36713
|
+
/**
|
|
36602
36714
|
* Build the JSON request body for an OpenAPI operation. When the operation
|
|
36603
|
-
*
|
|
36715
|
+
* names a Swagger body argument containing an object, send that object directly.
|
|
36716
|
+
* Otherwise, every payload key that is NOT a declared path/query/
|
|
36604
36717
|
* header param becomes the body. Sets `content-type` when a body is produced.
|
|
36605
36718
|
* Returns `undefined` for body-less operations (e.g. GET).
|
|
36606
36719
|
*/
|
|
36607
36720
|
function buildRequestBody(operation, args, headers) {
|
|
36608
36721
|
if (operation.requestBodySchema === void 0) return void 0;
|
|
36722
|
+
const namedBody = operation.bodyArgName === void 0 ? void 0 : args[operation.bodyArgName];
|
|
36723
|
+
if (namedBody !== null && typeof namedBody === "object") {
|
|
36724
|
+
headers["content-type"] = "application/json";
|
|
36725
|
+
return JSON.stringify(namedBody);
|
|
36726
|
+
}
|
|
36609
36727
|
const consumed = /* @__PURE__ */ new Set();
|
|
36610
36728
|
for (const param of operation.parameters ?? []) if (param.in === "path" || param.in === "query" || param.in === "header") consumed.add(param.name);
|
|
36611
36729
|
for (const arg of brokerConsumedArgs(operation)) consumed.add(arg.name);
|
|
@@ -36786,6 +36904,41 @@ function fail(error) {
|
|
|
36786
36904
|
error
|
|
36787
36905
|
};
|
|
36788
36906
|
}
|
|
36907
|
+
/**
|
|
36908
|
+
* A PROVIDER ROUND-TRIP that failed with an outcome nobody here can determine,
|
|
36909
|
+
* classified against the caller's own idempotency assertion (PLA-1349).
|
|
36910
|
+
*
|
|
36911
|
+
* `execute_failed` and `timeout` are the two retryable codes in
|
|
36912
|
+
* `BROKER_ERROR_RETRYABILITY`, and that default is right: a transport failure or
|
|
36913
|
+
* a gateway 5xx usually did not land. But they are also the only two where it
|
|
36914
|
+
* MIGHT have — the provider committed and the response was lost, or the deadline
|
|
36915
|
+
* fired after the write was accepted and the abort only closed the socket. A
|
|
36916
|
+
* durable step is retried whole, so retrying one of these re-sends the write and
|
|
36917
|
+
* one run creates two objects.
|
|
36918
|
+
*
|
|
36919
|
+
* Which of the two it is cannot be read off the code, the status or the connector
|
|
36920
|
+
* definition: `commentCreate` and `listComments` fail identically, and an
|
|
36921
|
+
* operation the provider deduplicates only deduplicates when the CALLER minted
|
|
36922
|
+
* the key. So the caller states it and this decides accordingly:
|
|
36923
|
+
*
|
|
36924
|
+
* - `false` → `retryable: false`. The SDK turns that into `fatal: true`, the
|
|
36925
|
+
* engine stops the step on its first failure, and a human reconciles. Same
|
|
36926
|
+
* trade the World already made for a dispatch past `httpTimeoutMs`.
|
|
36927
|
+
* - `true`, or NOT STATED → the code's own default, untouched. Passing
|
|
36928
|
+
* `undefined` as the override is byte-identical to omitting the argument, which
|
|
36929
|
+
* is what keeps an unstated option a true no-op.
|
|
36930
|
+
*
|
|
36931
|
+
* ONLY these two codes go through here. A code that was already terminal stays
|
|
36932
|
+
* terminal with no second decision, and a retryable code whose outcome is KNOWN —
|
|
36933
|
+
* a 401 the provider refused before doing anything, a stale-spec refusal, a 429
|
|
36934
|
+
* a rate limiter answered in front of the operation, a 408 for a request the
|
|
36935
|
+
* provider never received whole — keeps its retry, because there is nothing to
|
|
36936
|
+
* reconcile. Callers pass `undefined` for those rather than the assertion; see
|
|
36937
|
+
* the provider non-2xx exit.
|
|
36938
|
+
*/
|
|
36939
|
+
function unknownOutcome(code, message, idempotent) {
|
|
36940
|
+
return makeBrokerError(code, message, idempotent === false ? false : void 0);
|
|
36941
|
+
}
|
|
36789
36942
|
function messageOf$6(err) {
|
|
36790
36943
|
return err instanceof Error ? err.message : String(err);
|
|
36791
36944
|
}
|
|
@@ -36840,7 +36993,7 @@ var init_connect_action_executor = __esmMin((() => {
|
|
|
36840
36993
|
this.resumableStallTimeoutMs = deps.resumableStallTimeoutMs ?? 45e3;
|
|
36841
36994
|
}
|
|
36842
36995
|
async execute(input) {
|
|
36843
|
-
const { orgId, runId, connectConnectorId, connectOperationId, payload } = input;
|
|
36996
|
+
const { orgId, runId, connectConnectorId, connectOperationId, payload, idempotent } = input;
|
|
36844
36997
|
const runDef = runId !== void 0 ? await this.deps.runConnectors?.resolve(runId, orgId, connectConnectorId) ?? null : null;
|
|
36845
36998
|
const record = await this.deps.connectorRows?.resolve(orgId, connectConnectorId) ?? null;
|
|
36846
36999
|
let def;
|
|
@@ -36903,13 +37056,13 @@ var init_connect_action_executor = __esmMin((() => {
|
|
|
36903
37056
|
if (followUp === void 0) return this.missingFromSpec(specEntry, cached, `operation "${connectOperationId}" continues as "${declared.operationId}", which is not on connector "${connectConnectorId}"`);
|
|
36904
37057
|
if (declared.operationId === operation.operationId) return fail(makeBrokerError("invalid_args", `operation "${connectOperationId}" declares itself as its own continuation; a two-phase call needs a second, different operation`));
|
|
36905
37058
|
}
|
|
36906
|
-
return this.runOpenApi(def, operation, payload, tokenResponse, caller, specEntry, cached, followUp);
|
|
37059
|
+
return this.runOpenApi(def, operation, payload, tokenResponse, caller, specEntry, cached, followUp, idempotent);
|
|
36907
37060
|
}
|
|
36908
37061
|
const tool = findTool(introspection.tools, connectOperationId);
|
|
36909
37062
|
if (tool === void 0) return this.missingFromSpec(specEntry, cached, `tool "${connectOperationId}" not found on connector "${connectConnectorId}"`);
|
|
36910
|
-
return this.runMcp(def, tool, payload, tokenResponse, caller);
|
|
37063
|
+
return this.runMcp(def, tool, payload, tokenResponse, caller, idempotent);
|
|
36911
37064
|
}
|
|
36912
|
-
async runOpenApi(def, operation, payload, token, caller, specEntry, cached, followUp) {
|
|
37065
|
+
async runOpenApi(def, operation, payload, token, caller, specEntry, cached, followUp, idempotent) {
|
|
36913
37066
|
const args = isRecord$1(payload) ? payload : {};
|
|
36914
37067
|
for (const phase of followUp === void 0 ? [operation] : [operation, followUp]) {
|
|
36915
37068
|
const refused = unfilteredDeleteError(def, phase, args);
|
|
@@ -36933,11 +37086,12 @@ var init_connect_action_executor = __esmMin((() => {
|
|
|
36933
37086
|
});
|
|
36934
37087
|
}
|
|
36935
37088
|
} catch (err) {
|
|
36936
|
-
if (err instanceof ActionTimeoutError) return fail(
|
|
36937
|
-
if (isTransferStalled(err)) return fail(
|
|
36938
|
-
if (err instanceof ContinuationError) return fail(makeBrokerError(err.code, err.message, err.retryable));
|
|
36939
|
-
if (err instanceof
|
|
36940
|
-
return fail(makeBrokerError(
|
|
37089
|
+
if (err instanceof ActionTimeoutError) return fail(unknownOutcome("timeout", `operation "${reported.operationId}" timed out`, idempotent));
|
|
37090
|
+
if (isTransferStalled(err)) return fail(unknownOutcome("timeout", `operation "${reported.operationId}" stalled mid-transfer: ${messageOf$6(err)}`, idempotent));
|
|
37091
|
+
if (err instanceof ContinuationError) return fail(makeBrokerError(err.code, err.message, idempotent === false ? false : err.retryable));
|
|
37092
|
+
if (err instanceof RawResponseError) return fail(makeBrokerError(err.code, err.message, idempotent === false ? false : void 0));
|
|
37093
|
+
if (err instanceof RawBodyError) return fail(makeBrokerError(err.code, err.message));
|
|
37094
|
+
return fail(unknownOutcome("execute_failed", messageOf$6(err), idempotent));
|
|
36941
37095
|
}
|
|
36942
37096
|
const classified = classifyProviderError(def, result.status, result.output, reported.operationId);
|
|
36943
37097
|
if (classified?.code === "access_denied") return fail(classified);
|
|
@@ -36948,8 +37102,13 @@ var init_connect_action_executor = __esmMin((() => {
|
|
|
36948
37102
|
const body = formatOperationBody(result.output);
|
|
36949
37103
|
if (specEntry !== null && cached?.servedFromCache() === true && isStaleSpecError(def, result.output)) return this.onStaleSpec(specEntry, `operation "${reported.operationId}" failed with status ${result.status} against a stale spec${body ? `: ${body}` : ""}`);
|
|
36950
37104
|
if (classified !== null) return fail(classified);
|
|
36951
|
-
|
|
37105
|
+
const code = brokerCodeForProviderStatus(result.status);
|
|
37106
|
+
const message = `operation "${reported.operationId}" failed with status ${result.status}${body ? `: ${body}` : ""}`;
|
|
37107
|
+
if (code !== "execute_failed") return fail(makeBrokerError(code, message));
|
|
37108
|
+
return fail(unknownOutcome("execute_failed", message, result.status === 429 || result.status === 408 ? void 0 : idempotent));
|
|
36952
37109
|
}
|
|
37110
|
+
const unmatched = unmatchedWriteError(operation, result.matchedRows, reported.operationId);
|
|
37111
|
+
if (unmatched !== null) return fail(unmatched);
|
|
36953
37112
|
return {
|
|
36954
37113
|
ok: true,
|
|
36955
37114
|
output: result.output
|
|
@@ -36974,7 +37133,7 @@ var init_connect_action_executor = __esmMin((() => {
|
|
|
36974
37133
|
...this.deps.maxRawResponseBytes !== void 0 ? { maxRawResponseBytes: this.deps.maxRawResponseBytes } : {}
|
|
36975
37134
|
};
|
|
36976
37135
|
}
|
|
36977
|
-
async runMcp(def, tool, payload, token, caller) {
|
|
37136
|
+
async runMcp(def, tool, payload, token, caller, idempotent) {
|
|
36978
37137
|
if (this.deps.mcp === void 0) return fail(makeBrokerError("execute_failed", `connector "${def.id}" is an MCP connector but no MCP caller was wired`));
|
|
36979
37138
|
let output;
|
|
36980
37139
|
try {
|
|
@@ -36985,9 +37144,9 @@ var init_connect_action_executor = __esmMin((() => {
|
|
|
36985
37144
|
token: token.token
|
|
36986
37145
|
}), this.timeoutMs);
|
|
36987
37146
|
} catch (err) {
|
|
36988
|
-
if (err instanceof ActionTimeoutError) return fail(
|
|
37147
|
+
if (err instanceof ActionTimeoutError) return fail(unknownOutcome("timeout", `tool "${tool.name}" timed out`, idempotent));
|
|
36989
37148
|
if (statusOf(err) === 401) return this.on401(def, caller, `tool "${tool.name}" returned 401`);
|
|
36990
|
-
return fail(
|
|
37149
|
+
return fail(unknownOutcome("execute_failed", messageOf$6(err), idempotent));
|
|
36991
37150
|
}
|
|
36992
37151
|
return {
|
|
36993
37152
|
ok: true,
|
|
@@ -51785,11 +51944,12 @@ var init_broker_refusal = __esmMin((() => {
|
|
|
51785
51944
|
case "access_denied": return 401;
|
|
51786
51945
|
case "invalid_args": return 400;
|
|
51787
51946
|
case "invalid_grant": return 400;
|
|
51947
|
+
case "precondition_failed":
|
|
51948
|
+
case "cardinality_mismatch": return 409;
|
|
51788
51949
|
case "credential_error":
|
|
51789
51950
|
case "execute_failed":
|
|
51790
51951
|
case "timeout":
|
|
51791
51952
|
case "invalid_output": return 502;
|
|
51792
|
-
default: return 502;
|
|
51793
51953
|
}
|
|
51794
51954
|
};
|
|
51795
51955
|
brokerRefusal = (err) => {
|
|
@@ -53105,7 +53265,8 @@ var init_support = __esmMin((() => {
|
|
|
53105
53265
|
args: z.record(z.string(), z.unknown()).default({}),
|
|
53106
53266
|
principal: principalSchema.default({ type: "app" }),
|
|
53107
53267
|
installationId: z.string().min(1).optional(),
|
|
53108
|
-
runId: z.string().min(1).optional()
|
|
53268
|
+
runId: z.string().min(1).optional(),
|
|
53269
|
+
idempotent: z.boolean().optional()
|
|
53109
53270
|
});
|
|
53110
53271
|
requireBrokerSignature = (secret) => async (c, next) => {
|
|
53111
53272
|
if (!verifyTokenRequestSignature(c, secret)) return c.json({
|
|
@@ -53123,13 +53284,15 @@ var init_support = __esmMin((() => {
|
|
|
53123
53284
|
ok: false,
|
|
53124
53285
|
error: {
|
|
53125
53286
|
code: error.code,
|
|
53126
|
-
message: error.message
|
|
53287
|
+
message: error.message,
|
|
53288
|
+
...error.retryable !== void 0 ? { retryable: error.retryable } : {}
|
|
53127
53289
|
}
|
|
53128
53290
|
};
|
|
53129
53291
|
switch (statusForBrokerCode(error.code)) {
|
|
53130
53292
|
case 400: return c.json(body, 400);
|
|
53131
53293
|
case 401: return c.json(body, 401);
|
|
53132
53294
|
case 404: return c.json(body, 404);
|
|
53295
|
+
case 409: return c.json(body, 409);
|
|
53133
53296
|
default: return c.json(body, 502);
|
|
53134
53297
|
}
|
|
53135
53298
|
};
|
|
@@ -53269,13 +53432,14 @@ var createConnectTokenRouter = (opts, sharedContext) => {
|
|
|
53269
53432
|
security: HMAC_ROUTE_SECURITY,
|
|
53270
53433
|
middleware: [requireBrokerSignature(opts.hmacSecret)],
|
|
53271
53434
|
summary: "Run one connector operation",
|
|
53272
|
-
description: "Mints the credential, applies it in the connector’s declared placement, calls the operation and hands back ONLY its output. Nothing about the credential — not the token, not its placement — crosses this boundary, which is the whole reason a workflow step calls the box instead of the provider.\n\n`operation` names an operation id (OpenAPI) or a tool (MCP) as `GET /api/connect/connectors/{connectorId}/operations` reports it. `runId` is the one field that changes WHICH connector answers: a run wired to a stand-in reaches the stand-in and never leaves the box, which is how an eval exercises a send without sending anything.\n\nA refusal comes back in TWO shapes on the same statuses: `{ ok: false, error }` when the executor completed and the provider or the broker said no, and the box’s `{ code, message }` envelope
|
|
53435
|
+
description: "Mints the credential, applies it in the connector’s declared placement, calls the operation and hands back ONLY its output. Nothing about the credential — not the token, not its placement — crosses this boundary, which is the whole reason a workflow step calls the box instead of the provider.\n\n`operation` names an operation id (OpenAPI) or a tool (MCP) as `GET /api/connect/connectors/{connectorId}/operations` reports it. `runId` is the one field that changes WHICH connector answers: a run wired to a stand-in reaches the stand-in and never leaves the box, which is how an eval exercises a send without sending anything.\n\nA refusal comes back in TWO shapes on the same statuses: `{ ok: false, error }` when the executor completed and the provider or the broker said no, including a broker error thrown before execution completes. Signature and payload validation failures use the box’s `{ code, message }` envelope.",
|
|
53273
53436
|
request: { body: jsonBody(named("ConnectExecuteRequest", executeRequestSchema), "The connector, the operation, and the args to run it with.") },
|
|
53274
53437
|
responses: {
|
|
53275
53438
|
200: jsonResponse(named("ConnectExecuteSuccess", connectExecuteSuccessSchema), "The operation’s own output, passed through untouched."),
|
|
53276
53439
|
400: executeRefusal("The body failed its schema (`invalid_payload`), or the broker refused the arguments (`invalid_args`, `invalid_grant`)."),
|
|
53277
53440
|
401: executeRefusal("The signature is missing or wrong (`access_denied`), or the connector has no usable authorization (`no_token`, `user_authorization_required`, `principal_required`)."),
|
|
53278
53441
|
404: executeRefusal("No such connector on this box, or no such operation on it (`not_found`, `connector_installation_required`)."),
|
|
53442
|
+
409: executeRefusal("The provider refused a guarded write: no rows matched (`precondition_failed`), or the requested row limit was exceeded (`cardinality_mismatch`). Terminal: change the guard or narrow the target before retrying."),
|
|
53279
53443
|
502: executeRefusal("The provider call failed (`execute_failed`, `timeout`, `credential_error`, `invalid_output`). Upstream, not the box.")
|
|
53280
53444
|
}
|
|
53281
53445
|
}), async (c) => {
|
|
@@ -53291,7 +53455,8 @@ var createConnectTokenRouter = (opts, sharedContext) => {
|
|
|
53291
53455
|
connectConnectorId: request.connector,
|
|
53292
53456
|
connectOperationId: request.operation,
|
|
53293
53457
|
payload: request.args,
|
|
53294
|
-
...request.runId !== void 0 ? { runId: request.runId } : {}
|
|
53458
|
+
...request.runId !== void 0 ? { runId: request.runId } : {},
|
|
53459
|
+
...request.idempotent !== void 0 ? { idempotent: request.idempotent } : {}
|
|
53295
53460
|
});
|
|
53296
53461
|
if (result.ok) return c.json({
|
|
53297
53462
|
ok: true,
|
|
@@ -53299,7 +53464,8 @@ var createConnectTokenRouter = (opts, sharedContext) => {
|
|
|
53299
53464
|
}, 200);
|
|
53300
53465
|
return answerExecuteFailure(c, result.error);
|
|
53301
53466
|
} catch (err) {
|
|
53302
|
-
return
|
|
53467
|
+
if (isBrokerError(err)) return answerExecuteFailure(c, err);
|
|
53468
|
+
throw err;
|
|
53303
53469
|
}
|
|
53304
53470
|
});
|
|
53305
53471
|
app.openapi(createRoute({
|
|
@@ -65380,6 +65546,30 @@ var init_serial_execution_coordinator = __esmMin((() => {
|
|
|
65380
65546
|
createSerialExecutionCoordinator = (gate) => {
|
|
65381
65547
|
const held = /* @__PURE__ */ new Map();
|
|
65382
65548
|
return {
|
|
65549
|
+
async beginStart(workflowName) {
|
|
65550
|
+
const owner = held.get(workflowName);
|
|
65551
|
+
if (!owner) return null;
|
|
65552
|
+
const abandon = () => {
|
|
65553
|
+
if (held.get(workflowName) === owner) held.delete(workflowName);
|
|
65554
|
+
};
|
|
65555
|
+
try {
|
|
65556
|
+
if (!await gate.markAttempt(workflowName, owner.token)) {
|
|
65557
|
+
abandon();
|
|
65558
|
+
return null;
|
|
65559
|
+
}
|
|
65560
|
+
} catch (err) {
|
|
65561
|
+
abandon();
|
|
65562
|
+
throw err;
|
|
65563
|
+
}
|
|
65564
|
+
return {
|
|
65565
|
+
noteStarted(runId) {
|
|
65566
|
+
if (held.get(workflowName) === owner) owner.runId = runId;
|
|
65567
|
+
},
|
|
65568
|
+
acknowledge: () => gate.acknowledge(workflowName, owner.token),
|
|
65569
|
+
requeue: (trigger) => gate.requeue(workflowName, trigger, owner.token),
|
|
65570
|
+
abandon
|
|
65571
|
+
};
|
|
65572
|
+
},
|
|
65383
65573
|
async tryAdmit(workflowName, trigger) {
|
|
65384
65574
|
const verdict = await gate.tryAdmit(workflowName, trigger);
|
|
65385
65575
|
if (verdict.admitted) {
|
|
@@ -65420,26 +65610,34 @@ var init_serial_execution_coordinator = __esmMin((() => {
|
|
|
65420
65610
|
held.delete(workflowName);
|
|
65421
65611
|
},
|
|
65422
65612
|
async requeue(workflowName, trigger) {
|
|
65423
|
-
|
|
65613
|
+
const owner = held.get(workflowName);
|
|
65614
|
+
if (owner) await gate.requeue(workflowName, trigger, owner.token);
|
|
65424
65615
|
}
|
|
65425
65616
|
};
|
|
65426
65617
|
};
|
|
65427
65618
|
createSerialSweepSupport = (deps) => {
|
|
65428
65619
|
const startAdmitted = async (name, trigger) => {
|
|
65620
|
+
const attempt = await deps.coordinator.beginStart(name);
|
|
65621
|
+
if (!attempt) return;
|
|
65429
65622
|
let started;
|
|
65430
65623
|
try {
|
|
65431
65624
|
started = await deps.startNext(name, trigger);
|
|
65432
65625
|
} catch (err) {
|
|
65433
65626
|
try {
|
|
65434
|
-
await
|
|
65627
|
+
await attempt.requeue(trigger);
|
|
65435
65628
|
} catch (requeueErr) {
|
|
65436
65629
|
console.warn(`[serial-execution] could not re-enqueue an unstarted trigger for "${name}": ${requeueErr instanceof Error ? requeueErr.message : String(requeueErr)}`);
|
|
65437
65630
|
}
|
|
65438
|
-
|
|
65439
|
-
console.warn(`[serial-execution] failed to start admitted trigger for "${name}"
|
|
65631
|
+
attempt.abandon();
|
|
65632
|
+
console.warn(`[serial-execution] failed to start admitted trigger for "${name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
65440
65633
|
return;
|
|
65441
65634
|
}
|
|
65442
|
-
|
|
65635
|
+
attempt.noteStarted(started?.worldRunId);
|
|
65636
|
+
try {
|
|
65637
|
+
await attempt.acknowledge();
|
|
65638
|
+
} catch (err) {
|
|
65639
|
+
console.warn(`[serial-execution] could not acknowledge started trigger for "${name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
65640
|
+
}
|
|
65443
65641
|
await deps.clearWaiting?.(name, trigger.pendingId);
|
|
65444
65642
|
};
|
|
65445
65643
|
return {
|
|
@@ -67254,9 +67452,12 @@ var init_event_forwarder = __esmMin((() => {
|
|
|
67254
67452
|
* the row `claimed` while the attempt went ahead; a later swallowed `markStarted`
|
|
67255
67453
|
* would then leave a `claimed` row with a real run behind it, the sweep would
|
|
67256
67454
|
* release it, and the customer's workflow would run a second time. Failing here
|
|
67257
|
-
*
|
|
67258
|
-
*
|
|
67259
|
-
* the
|
|
67455
|
+
* instead bubbles without releasing: a failed marker cannot establish ownership,
|
|
67456
|
+
* and deleting by pair could erase another writer's delivery. The tick holds its
|
|
67457
|
+
* cursor. If the row remains `claimed`, recovery waits for the takeover window
|
|
67458
|
+
* and consumes takeover budget, even for a transient marker failure. An applied
|
|
67459
|
+
* update whose response is lost instead leaves `starting` without an attempt;
|
|
67460
|
+
* this slice cannot resolve that unknown outcome (PLA-1395 investigation).
|
|
67260
67461
|
*
|
|
67261
67462
|
* Written at most once: a resume that misses falls through to the start below.
|
|
67262
67463
|
*/
|
|
@@ -67265,13 +67466,7 @@ var init_event_forwarder = __esmMin((() => {
|
|
|
67265
67466
|
* one must give up where it stands. */
|
|
67266
67467
|
const markAttempt = async () => {
|
|
67267
67468
|
if (attemptMarked) return true;
|
|
67268
|
-
|
|
67269
|
-
try {
|
|
67270
|
-
matched = await deps.deliveries.markStarting(subscription.id, event.id);
|
|
67271
|
-
} catch (err) {
|
|
67272
|
-
await bestEffort(() => deps.deliveries.release(subscription.id, event.id), "release");
|
|
67273
|
-
throw err;
|
|
67274
|
-
}
|
|
67469
|
+
const matched = await deps.deliveries.markStarting(subscription.id, event.id);
|
|
67275
67470
|
attemptMarked = matched;
|
|
67276
67471
|
return matched;
|
|
67277
67472
|
};
|
|
@@ -70958,7 +71153,7 @@ var calls_workflow_ts_default = "import { z } from \"zod\";\nimport { streamDeep
|
|
|
70958
71153
|
var plain_workflow_ts_default = "import { z } from \"zod\";\n\n/**\n * Workflow \"{{name}}\" — a durable pipeline.\n *\n * Each `\"use step\"` is a durable checkpoint: kill the runtime mid-run and it\n * resumes from the last completed step instead of replaying everything.\n */\n\n// THE contract — sibling exports next to the bare workflow function. The build\n// harvests these (by convention) into the manifest + a live validator. The\n// parameter type is DERIVED from `inputSchema` and the RETURN type from\n// `outputSchema` (both `z.infer`), so neither side can drift from the contract\n// the build harvests. Without the return annotation the schema is published to\n// Studio and the generated types and never compared to what the body returns\n// (PLA-1272).\nexport const inputSchema = z.object({\n message: z.string().describe(\"The input to process\"),\n});\n\nexport const outputSchema = z.object({\n message: z.string(),\n processed: z.boolean(),\n});\n\ntype {{Name}}Input = z.infer<typeof inputSchema>;\n\nexport async function {{export}}(input: {{Name}}Input): Promise<z.infer<typeof outputSchema>> {\n \"use workflow\";\n\n const result = await processMessage(input); // step 1 — durable checkpoint\n return result;\n}\n\nasync function processMessage(input: {{Name}}Input) {\n \"use step\";\n // Replace this with the deterministic work of your step (DB write, HTTP call,\n // an LLM call, …). Returning a value records it as the step's checkpoint.\n return { message: input.message, processed: true };\n}\n";
|
|
70959
71154
|
//#endregion
|
|
70960
71155
|
//#region src/services/scaffold/template-loader.ts
|
|
70961
|
-
var SDK_TEMPLATE_VERSION = "0.4.
|
|
71156
|
+
var SDK_TEMPLATE_VERSION = "0.4.1";
|
|
70962
71157
|
var PREFIX = "./templates/";
|
|
70963
71158
|
var templates = new Map(Object.entries(/* @__PURE__ */ Object.assign({
|
|
70964
71159
|
"./templates/deep-agents/index.ts.tmpl": index_ts_default,
|
|
@@ -78802,6 +78997,7 @@ var generateConnectorTypes = (models) => {
|
|
|
78802
78997
|
HEADER,
|
|
78803
78998
|
"",
|
|
78804
78999
|
...AUGMENTED_MODULES.map((mod) => `import ${JSON.stringify(mod)};`),
|
|
79000
|
+
`import type { CallConnectorOptions } from ${JSON.stringify(AUGMENTED_MODULES[0])};`,
|
|
78805
79001
|
"",
|
|
78806
79002
|
augmentations.join("\n"),
|
|
78807
79003
|
""
|
|
@@ -78817,15 +79013,30 @@ function renderConnectorEntry(model) {
|
|
|
78817
79013
|
].join("\n");
|
|
78818
79014
|
}
|
|
78819
79015
|
/**
|
|
78820
|
-
* One ` <op>(args: <type
|
|
78821
|
-
* THREE overload lines a `'when-requested'` raw response
|
|
78822
|
-
* signature there has two reachable outcomes.
|
|
79016
|
+
* One ` <op>(args: <type>, opts?: CallConnectorOptions): Promise<<out>>;` line
|
|
79017
|
+
* (indent level 3) — or the THREE overload lines a `'when-requested'` raw response
|
|
79018
|
+
* takes, because one signature there has two reachable outcomes.
|
|
78823
79019
|
*/
|
|
78824
79020
|
function renderOperation(op) {
|
|
78825
79021
|
if (takesEitherOutcome(op)) return renderEitherOutcome(op);
|
|
78826
|
-
return ` ${propertyKey(op.name)}(${renderParam(op)}): Promise<${renderReturn(op)}>;`;
|
|
79022
|
+
return ` ${propertyKey(op.name)}(${renderParam(op)}${OPTS_PARAM}): Promise<${renderReturn(op)}>;`;
|
|
78827
79023
|
}
|
|
78828
79024
|
/**
|
|
79025
|
+
* The second parameter every generated signature carries (PLA-1349).
|
|
79026
|
+
*
|
|
79027
|
+
* `connection(id).<op>(args, opts)` has always forwarded a second argument at run
|
|
79028
|
+
* time (`call-connector.ts`), and no generated signature declared it — so
|
|
79029
|
+
* `idempotent` was reachable only through the untyped `.call(...)`, which is not
|
|
79030
|
+
* the path a creator is steered to. Typed as the whole options object rather than
|
|
79031
|
+
* one field, because PLA-1351's `echoPath` lands in exactly the same place.
|
|
79032
|
+
*
|
|
79033
|
+
* It is ALWAYS second, and `renderParam` is what guarantees that: an operation
|
|
79034
|
+
* with nothing to put in `args` still prints `args?: undefined` ahead of this. The
|
|
79035
|
+
* proxy is `(args?, opts?)` for every operation, so a signature that let `opts`
|
|
79036
|
+
* come first would hand the caller's options to the provider as arguments.
|
|
79037
|
+
*/
|
|
79038
|
+
var OPTS_PARAM = ", opts?: CallConnectorOptions";
|
|
79039
|
+
/**
|
|
78829
79040
|
* Does ONE signature of this operation have two reachable outcomes?
|
|
78830
79041
|
*
|
|
78831
79042
|
* Only `'when-requested'`, and only while the switch is actually IN its arguments
|
|
@@ -78865,7 +79076,7 @@ function takesEitherOutcome(op) {
|
|
|
78865
79076
|
function renderEitherOutcome(op) {
|
|
78866
79077
|
const receipt = printType(rawResponseReceiptJsonSchema(), 3);
|
|
78867
79078
|
const response = op.outputSchema === void 0 ? "unknown" : printType(op.outputSchema, 3);
|
|
78868
|
-
const signature = (presence, out) => ` ${propertyKey(op.name)}(args: ${printType(withKeyPresence(op.argsSchema, presence), 3)}): Promise<${out}>;`;
|
|
79079
|
+
const signature = (presence, out) => ` ${propertyKey(op.name)}(args: ${printType(withKeyPresence(op.argsSchema, presence), 3)}${OPTS_PARAM}): Promise<${out}>;`;
|
|
78869
79080
|
return [
|
|
78870
79081
|
signature("required", receipt),
|
|
78871
79082
|
signature("forbidden", response),
|
|
@@ -78881,6 +79092,8 @@ function renderEitherOutcome(op) {
|
|
|
78881
79092
|
*/
|
|
78882
79093
|
function withKeyPresence(schema, presence) {
|
|
78883
79094
|
if (!isObject(schema)) return schema;
|
|
79095
|
+
const union = schema["anyOf"] ?? schema["oneOf"];
|
|
79096
|
+
if (Array.isArray(union)) return { anyOf: union.map((branch) => withKeyPresence(branch, presence)) };
|
|
78884
79097
|
const properties = { ...isObject(schema["properties"]) ? schema["properties"] : {} };
|
|
78885
79098
|
const required = new Set(Array.isArray(schema["required"]) ? schema["required"].filter((k) => typeof k === "string") : []);
|
|
78886
79099
|
if (presence === "required") required.add(RAW_RESPONSE_KEY_ARG);
|
|
@@ -78896,7 +79109,7 @@ function withKeyPresence(schema, presence) {
|
|
|
78896
79109
|
}
|
|
78897
79110
|
/** `args: <type>` for an operation with arguments, and nothing for one without. */
|
|
78898
79111
|
function renderParam(op) {
|
|
78899
|
-
return schemaHasProperties(op.argsSchema) ? `args: ${printType(op.argsSchema, 3)}` : "";
|
|
79112
|
+
return schemaHasProperties(op.argsSchema) ? `args: ${printType(op.argsSchema, 3)}` : "args?: undefined";
|
|
78900
79113
|
}
|
|
78901
79114
|
/**
|
|
78902
79115
|
* What the operation ACTUALLY answers (PLA-1203).
|
|
@@ -79142,8 +79355,9 @@ function answeredResponseSchema(op, byId) {
|
|
|
79142
79355
|
* BROKER-consumed arguments into ONE flat args object schema — exactly how the
|
|
79143
79356
|
* broker executor reads the payload (`args[param.name]` for path/query, the
|
|
79144
79357
|
* broker's own keys off the raw flags, the rest as the JSON body). A non-object
|
|
79145
|
-
* request body (rare) is dropped from the flat shape
|
|
79146
|
-
*
|
|
79358
|
+
* request body (rare) is dropped from the flat shape. Swagger's named body is
|
|
79359
|
+
* also offered as an optional argument for writes whose column names overlap
|
|
79360
|
+
* query filters.
|
|
79147
79361
|
*
|
|
79148
79362
|
* The three sources are merged in the order the executor reads them, and the
|
|
79149
79363
|
* broker's come LAST because they are the only ones it will not put on the wire.
|
|
@@ -79167,19 +79381,44 @@ function operationArgsSchema(op, trustBodyRequired) {
|
|
|
79167
79381
|
Object.assign(properties, bodyProperties);
|
|
79168
79382
|
if (trustBodyRequired && Array.isArray(body["required"])) required.push(...body["required"].filter((k) => typeof k === "string" && !isServerGenerated(bodyProperties[k])));
|
|
79169
79383
|
}
|
|
79384
|
+
const bodyRequired = [...required];
|
|
79385
|
+
const sharedRequired = [];
|
|
79386
|
+
const consumedNames = /* @__PURE__ */ new Set();
|
|
79387
|
+
if (op.bodyArgName !== void 0 && isObject(body)) {
|
|
79388
|
+
const namedBody = { ...body };
|
|
79389
|
+
if (!trustBodyRequired) delete namedBody["required"];
|
|
79390
|
+
else if (Array.isArray(body["required"]) && isObject(body["properties"])) {
|
|
79391
|
+
const bodyProperties = body["properties"];
|
|
79392
|
+
namedBody["required"] = body["required"].filter((key) => typeof key === "string" && !isServerGenerated(bodyProperties[key]));
|
|
79393
|
+
}
|
|
79394
|
+
properties[op.bodyArgName] = namedBody;
|
|
79395
|
+
}
|
|
79170
79396
|
for (const param of op.parameters ?? []) {
|
|
79171
79397
|
properties[param.name] = param.schema ?? {};
|
|
79172
|
-
|
|
79398
|
+
consumedNames.add(param.name);
|
|
79399
|
+
if (param.required) {
|
|
79400
|
+
required.push(param.name);
|
|
79401
|
+
sharedRequired.push(param.name);
|
|
79402
|
+
}
|
|
79173
79403
|
}
|
|
79174
79404
|
for (const arg of brokerConsumedArgs(op)) {
|
|
79175
79405
|
properties[arg.name] = arg.schema;
|
|
79176
|
-
|
|
79406
|
+
consumedNames.add(arg.name);
|
|
79407
|
+
if (arg.required) {
|
|
79408
|
+
required.push(arg.name);
|
|
79409
|
+
sharedRequired.push(arg.name);
|
|
79410
|
+
}
|
|
79177
79411
|
}
|
|
79178
|
-
|
|
79412
|
+
const flat = {
|
|
79179
79413
|
type: "object",
|
|
79180
79414
|
properties,
|
|
79181
79415
|
...required.length > 0 ? { required: [...new Set(required)] } : {}
|
|
79182
79416
|
};
|
|
79417
|
+
if (op.bodyArgName === void 0 || bodyRequired.length === 0 || consumedNames.has(op.bodyArgName)) return flat;
|
|
79418
|
+
return { anyOf: [flat, {
|
|
79419
|
+
...flat,
|
|
79420
|
+
required: [...new Set([...sharedRequired, op.bodyArgName])]
|
|
79421
|
+
}] };
|
|
79183
79422
|
}
|
|
79184
79423
|
/**
|
|
79185
79424
|
* Whether a body property is one the SERVER produces rather than one the caller
|
|
@@ -79202,11 +79441,17 @@ function isServerGenerated(property) {
|
|
|
79202
79441
|
}
|
|
79203
79442
|
/** Whether an object schema declares ONE named property. */
|
|
79204
79443
|
function schemaHasProperty(schema, name) {
|
|
79205
|
-
|
|
79444
|
+
if (!isObject(schema)) return false;
|
|
79445
|
+
const union = schema["anyOf"] ?? schema["oneOf"];
|
|
79446
|
+
if (Array.isArray(union)) return union.some((branch) => schemaHasProperty(branch, name));
|
|
79447
|
+
return isObject(schema["properties"]) && name in schema["properties"];
|
|
79206
79448
|
}
|
|
79207
79449
|
/** Whether an object schema declares at least one property (else the op takes no args). */
|
|
79208
79450
|
function schemaHasProperties(schema) {
|
|
79209
|
-
|
|
79451
|
+
if (!isObject(schema)) return false;
|
|
79452
|
+
const union = schema["anyOf"] ?? schema["oneOf"];
|
|
79453
|
+
if (Array.isArray(union)) return union.some(schemaHasProperties);
|
|
79454
|
+
return isObject(schema["properties"]) && Object.keys(schema["properties"]).length > 0;
|
|
79210
79455
|
}
|
|
79211
79456
|
/**
|
|
79212
79457
|
* Print a TypeScript type literal for a JSON Schema node. Handles the subset
|
|
@@ -81823,6 +82068,64 @@ var createDevCommand = (ctx) => defineCommand({
|
|
|
81823
82068
|
})(args)
|
|
81824
82069
|
});
|
|
81825
82070
|
//#endregion
|
|
82071
|
+
//#region ../../libs/shared/format/src/lib/coding-agent-prompt.ts
|
|
82072
|
+
/**
|
|
82073
|
+
* The pasteable prompt, and nothing around it, with no "open your coding agent"
|
|
82074
|
+
* preamble. Each surface draws its own instruction: the CLI as a `note`, the
|
|
82075
|
+
* web as the label above a copy button.
|
|
82076
|
+
*
|
|
82077
|
+
* `null` is the empty workspace, which is what the web always passes: the
|
|
82078
|
+
* browser cannot see which piece the reader picked at `init`'s own prompt. That
|
|
82079
|
+
* costs nothing, because `stackbone-coder` detects the workspace on disk by
|
|
82080
|
+
* itself and adds to it rather than re-scaffolding.
|
|
82081
|
+
*/
|
|
82082
|
+
function codingAgentPrompt(piece) {
|
|
82083
|
+
if (!piece) return "Use the stackbone-coder skill to build my first piece in this workspace: interview me about what it should do, then wire it up.";
|
|
82084
|
+
return `Use the stackbone-coder skill. The "${piece.name}" ${piece.kind} is already scaffolded in this workspace, so skip the scaffolding: interview me about what it should do, then wire it up.`;
|
|
82085
|
+
}
|
|
82086
|
+
//#endregion
|
|
82087
|
+
//#region ../../libs/shared/format/src/lib/coding-agents.ts
|
|
82088
|
+
var CODING_AGENT_LABELS = [
|
|
82089
|
+
{
|
|
82090
|
+
id: "claude-code",
|
|
82091
|
+
label: "Claude Code"
|
|
82092
|
+
},
|
|
82093
|
+
{
|
|
82094
|
+
id: "cursor",
|
|
82095
|
+
label: "Cursor"
|
|
82096
|
+
},
|
|
82097
|
+
{
|
|
82098
|
+
id: "github-copilot",
|
|
82099
|
+
label: "GitHub Copilot (VS Code)"
|
|
82100
|
+
},
|
|
82101
|
+
{
|
|
82102
|
+
id: "gemini-cli",
|
|
82103
|
+
label: "Gemini CLI"
|
|
82104
|
+
},
|
|
82105
|
+
{
|
|
82106
|
+
id: "windsurf",
|
|
82107
|
+
label: "Windsurf"
|
|
82108
|
+
},
|
|
82109
|
+
{
|
|
82110
|
+
id: "cline",
|
|
82111
|
+
label: "Cline"
|
|
82112
|
+
},
|
|
82113
|
+
{
|
|
82114
|
+
id: "codex",
|
|
82115
|
+
label: "Codex CLI"
|
|
82116
|
+
}
|
|
82117
|
+
];
|
|
82118
|
+
/**
|
|
82119
|
+
* Throws rather than falling back to the id: a registry row whose label is
|
|
82120
|
+
* missing here is a mistake to fix at build time, not a menu entry reading
|
|
82121
|
+
* `github-copilot`.
|
|
82122
|
+
*/
|
|
82123
|
+
var codingAgentLabel = (id) => {
|
|
82124
|
+
const found = CODING_AGENT_LABELS.find((agent) => agent.id === id);
|
|
82125
|
+
if (!found) throw new Error(`no label for coding agent "${id}"`);
|
|
82126
|
+
return found.label;
|
|
82127
|
+
};
|
|
82128
|
+
//#endregion
|
|
81826
82129
|
//#region src/services/coding-agents/registry.ts
|
|
81827
82130
|
/**
|
|
81828
82131
|
* The one place that knows which coding agents Stackbone supports and what
|
|
@@ -81831,6 +82134,10 @@ var createDevCommand = (ctx) => defineCommand({
|
|
|
81831
82134
|
* that pre-ticks the menu, and the MCP config writer — so adding an agent is a
|
|
81832
82135
|
* row here and nothing else.
|
|
81833
82136
|
*
|
|
82137
|
+
* The one thing a row does NOT own is its label: that is in `@stackbone/format`,
|
|
82138
|
+
* because the browser renders the same list and cannot import this file — it
|
|
82139
|
+
* resolves paths against `homedir()` and reads `process.platform`.
|
|
82140
|
+
*
|
|
81834
82141
|
* The four DIFFERENT MCP entry shapes below are the reason this is data rather
|
|
81835
82142
|
* than a switch. There is no convention: `mcpServers` vs `servers`, `url` vs
|
|
81836
82143
|
* `httpUrl` vs `serverUrl`, `http` vs `streamableHttp`, JSON vs TOML. Each was
|
|
@@ -81870,7 +82177,7 @@ var CODEX_MCP = {
|
|
|
81870
82177
|
var CODING_AGENTS = [
|
|
81871
82178
|
{
|
|
81872
82179
|
id: "claude-code",
|
|
81873
|
-
label: "
|
|
82180
|
+
label: codingAgentLabel("claude-code"),
|
|
81874
82181
|
skillsId: "claude-code",
|
|
81875
82182
|
skillsDir: ".claude",
|
|
81876
82183
|
detect: [".claude", "~/.claude"],
|
|
@@ -81884,7 +82191,7 @@ var CODING_AGENTS = [
|
|
|
81884
82191
|
},
|
|
81885
82192
|
{
|
|
81886
82193
|
id: "cursor",
|
|
81887
|
-
label: "
|
|
82194
|
+
label: codingAgentLabel("cursor"),
|
|
81888
82195
|
skillsId: "cursor",
|
|
81889
82196
|
skillsDir: ".agents",
|
|
81890
82197
|
detect: [".cursor", "~/.cursor"],
|
|
@@ -81898,7 +82205,7 @@ var CODING_AGENTS = [
|
|
|
81898
82205
|
},
|
|
81899
82206
|
{
|
|
81900
82207
|
id: "github-copilot",
|
|
81901
|
-
label: "
|
|
82208
|
+
label: codingAgentLabel("github-copilot"),
|
|
81902
82209
|
skillsId: "github-copilot",
|
|
81903
82210
|
skillsDir: ".agents",
|
|
81904
82211
|
detect: [".vscode"],
|
|
@@ -81912,7 +82219,7 @@ var CODING_AGENTS = [
|
|
|
81912
82219
|
},
|
|
81913
82220
|
{
|
|
81914
82221
|
id: "gemini-cli",
|
|
81915
|
-
label: "
|
|
82222
|
+
label: codingAgentLabel("gemini-cli"),
|
|
81916
82223
|
skillsId: "gemini-cli",
|
|
81917
82224
|
skillsDir: ".agents",
|
|
81918
82225
|
detect: [".gemini", "~/.gemini"],
|
|
@@ -81926,7 +82233,7 @@ var CODING_AGENTS = [
|
|
|
81926
82233
|
},
|
|
81927
82234
|
{
|
|
81928
82235
|
id: "windsurf",
|
|
81929
|
-
label: "
|
|
82236
|
+
label: codingAgentLabel("windsurf"),
|
|
81930
82237
|
skillsId: "windsurf",
|
|
81931
82238
|
skillsDir: ".windsurf",
|
|
81932
82239
|
detect: [".windsurf", "~/.codeium"],
|
|
@@ -81944,7 +82251,7 @@ var CODING_AGENTS = [
|
|
|
81944
82251
|
},
|
|
81945
82252
|
{
|
|
81946
82253
|
id: "cline",
|
|
81947
|
-
label: "
|
|
82254
|
+
label: codingAgentLabel("cline"),
|
|
81948
82255
|
skillsId: "cline",
|
|
81949
82256
|
skillsDir: ".agents",
|
|
81950
82257
|
detect: [
|
|
@@ -81965,7 +82272,7 @@ var CODING_AGENTS = [
|
|
|
81965
82272
|
},
|
|
81966
82273
|
{
|
|
81967
82274
|
id: "codex",
|
|
81968
|
-
label: "
|
|
82275
|
+
label: codingAgentLabel("codex"),
|
|
81969
82276
|
skillsId: "codex",
|
|
81970
82277
|
skillsDir: ".agents",
|
|
81971
82278
|
detect: [".codex", "~/.codex"],
|
|
@@ -82642,22 +82949,6 @@ var createHitlCommand = defineGroup({
|
|
|
82642
82949
|
}
|
|
82643
82950
|
});
|
|
82644
82951
|
//#endregion
|
|
82645
|
-
//#region ../../libs/shared/format/src/lib/coding-agent-prompt.ts
|
|
82646
|
-
/**
|
|
82647
|
-
* The pasteable prompt, and nothing around it, with no "open your coding agent"
|
|
82648
|
-
* preamble. Each surface draws its own instruction: the CLI as a `note`, the
|
|
82649
|
-
* web as the label above a copy button.
|
|
82650
|
-
*
|
|
82651
|
-
* `null` is the empty workspace, which is what the web always passes: the
|
|
82652
|
-
* browser cannot see which piece the reader picked at `init`'s own prompt. That
|
|
82653
|
-
* costs nothing, because `stackbone-coder` detects the workspace on disk by
|
|
82654
|
-
* itself and adds to it rather than re-scaffolding.
|
|
82655
|
-
*/
|
|
82656
|
-
function codingAgentPrompt(piece) {
|
|
82657
|
-
if (!piece) return "Use the stackbone-coder skill to build my first piece in this workspace: interview me about what it should do, then wire it up.";
|
|
82658
|
-
return `Use the stackbone-coder skill. The "${piece.name}" ${piece.kind} is already scaffolded in this workspace, so skip the scaffolding: interview me about what it should do, then wire it up.`;
|
|
82659
|
-
}
|
|
82660
|
-
//#endregion
|
|
82661
82952
|
//#region src/core/npm-exec.ts
|
|
82662
82953
|
var execFileAsync = promisify(execFile);
|
|
82663
82954
|
var NEEDS_SHELL = process.platform === "win32";
|