@stackbone/cli 0.4.0 → 0.5.0
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 +72 -0
- package/main.js +613 -162
- package/package.json +1 -1
- package/runtime-deps/@stackbone/workflow-world/index.js +48 -13
- package/stackbone-cli-0.5.0.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,20 @@ 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, echoPath }`. `principal`
|
|
9724
|
+
* defaults to `{ type: 'app' }`; `idempotent` is UNSET by
|
|
9725
|
+
* default and only an explicit `false` changes how a failure is
|
|
9726
|
+
* retried; `echoPath` records what this write returned so a
|
|
9727
|
+
* later delivery of it can be recognised as our own.
|
|
9698
9728
|
*/
|
|
9699
9729
|
async function callConnector(connector, operation, args, opts) {
|
|
9700
9730
|
return (await getConnectorInvoker().invoke({
|
|
9701
9731
|
connector,
|
|
9702
9732
|
operation,
|
|
9703
9733
|
...args !== void 0 ? { args } : {},
|
|
9704
|
-
...opts?.principal !== void 0 ? { principal: opts.principal } : {}
|
|
9734
|
+
...opts?.principal !== void 0 ? { principal: opts.principal } : {},
|
|
9735
|
+
...opts?.idempotent !== void 0 ? { idempotent: opts.idempotent } : {},
|
|
9736
|
+
...opts?.echoPath !== void 0 ? { echoPath: opts.echoPath } : {}
|
|
9705
9737
|
})).output;
|
|
9706
9738
|
}
|
|
9707
9739
|
var connectorHandle, connection;
|
|
@@ -14339,6 +14371,29 @@ var createInvocationsStore = (opts) => {
|
|
|
14339
14371
|
]);
|
|
14340
14372
|
return rows[0]?.id ?? null;
|
|
14341
14373
|
},
|
|
14374
|
+
async findEchoRecord(input) {
|
|
14375
|
+
assertOpen();
|
|
14376
|
+
const args = [
|
|
14377
|
+
input.connector,
|
|
14378
|
+
input.value,
|
|
14379
|
+
input.windowMs
|
|
14380
|
+
];
|
|
14381
|
+
let markerScope = "";
|
|
14382
|
+
if (input.workflowName !== null) {
|
|
14383
|
+
args.push(input.workflowName);
|
|
14384
|
+
markerScope = ` AND (input->>'workflowName' = $${args.length} OR input->>'workflowName' IS NULL)`;
|
|
14385
|
+
}
|
|
14386
|
+
const { rows } = await readPool.query(`SELECT bool_or(input->>'echo' = $2) AS matched,
|
|
14387
|
+
bool_or(input->>'echoUnresolved' = 'true'${markerScope}) AS unresolved
|
|
14388
|
+
FROM stackbone_platform.run_steps
|
|
14389
|
+
WHERE type = 'connection'
|
|
14390
|
+
AND input->>'connector' = $1
|
|
14391
|
+
AND created_at > now() - ($3::bigint * interval '1 millisecond')`, args);
|
|
14392
|
+
return {
|
|
14393
|
+
matched: rows[0]?.matched === true,
|
|
14394
|
+
unresolved: rows[0]?.unresolved === true
|
|
14395
|
+
};
|
|
14396
|
+
},
|
|
14342
14397
|
async annotatePromptUse(input) {
|
|
14343
14398
|
if (!isUuid(input.runId)) return;
|
|
14344
14399
|
assertOpen();
|
|
@@ -15221,7 +15276,8 @@ var createTriggerSubscriptionsStore = (opts) => {
|
|
|
15221
15276
|
assertOpen();
|
|
15222
15277
|
await pool.query(`UPDATE stackbone_platform.trigger_deliveries
|
|
15223
15278
|
SET status = 'rejected', issues = $3::jsonb, updated_at = now()
|
|
15224
|
-
WHERE subscription_id = $1::uuid AND provider_item_id = $2
|
|
15279
|
+
WHERE subscription_id = $1::uuid AND provider_item_id = $2
|
|
15280
|
+
AND status IN ('claimed', 'starting')`, [
|
|
15225
15281
|
subscriptionId,
|
|
15226
15282
|
providerItemId,
|
|
15227
15283
|
JSON.stringify(issues)
|
|
@@ -15669,7 +15725,7 @@ var emulatorCors = (allowlist) => cors({
|
|
|
15669
15725
|
var STACKBONE_CLI_BUILD_VERSION, EMULATOR_CAPABILITY_ROUTES, EMULATOR_CAPABILITIES, buildContractResponse;
|
|
15670
15726
|
var init_contract = __esmMin((() => {
|
|
15671
15727
|
init_src$19();
|
|
15672
|
-
STACKBONE_CLI_BUILD_VERSION = "0.
|
|
15728
|
+
STACKBONE_CLI_BUILD_VERSION = "0.5.0";
|
|
15673
15729
|
EMULATOR_CAPABILITY_ROUTES = {
|
|
15674
15730
|
"database.postgres_direct": "/api/db",
|
|
15675
15731
|
"rag.basic": "/api/rag",
|
|
@@ -30692,6 +30748,8 @@ var init_types$1 = __esmMin((() => {
|
|
|
30692
30748
|
description: z.string().optional(),
|
|
30693
30749
|
parameters: z.array(operationParameterSchema).optional(),
|
|
30694
30750
|
requestBodySchema: z.unknown().optional(),
|
|
30751
|
+
/** Original Swagger body parameter name; allows body values alongside query filters. */
|
|
30752
|
+
bodyArgName: z.string().optional(),
|
|
30695
30753
|
responseSchema: z.unknown().optional(),
|
|
30696
30754
|
/**
|
|
30697
30755
|
* This operation's body is BYTES, not JSON. The executor then resolves the
|
|
@@ -31615,7 +31673,9 @@ var init_types = __esmMin((() => {
|
|
|
31615
31673
|
/** A policy denial on a VALID key: no credential eviction, no retry loop. */
|
|
31616
31674
|
accessDenied: z.array(z.string()).default([]),
|
|
31617
31675
|
/** A data conflict on a write (unique / FK violation): terminal, handleable. */
|
|
31618
|
-
conflict: z.array(z.string()).default([])
|
|
31676
|
+
conflict: z.array(z.string()).default([]),
|
|
31677
|
+
/** The provider rejected a write that exceeded its requested row limit. */
|
|
31678
|
+
cardinalityMismatch: z.array(z.string()).optional()
|
|
31619
31679
|
}).optional(),
|
|
31620
31680
|
/**
|
|
31621
31681
|
* The query parameter that carries "which columns to return" on this
|
|
@@ -32269,8 +32329,8 @@ var init_github_connector = __esmMin((() => {
|
|
|
32269
32329
|
triggers: [{
|
|
32270
32330
|
id: "commit-pushed",
|
|
32271
32331
|
displayName: "New commit by date",
|
|
32272
|
-
description: "Starts one run per commit whose committer date is newer than this link’s cursor. This follows commit dates, not push times: pushing an older local commit or merging existing commits without changing their committer dates can leave them undetected. A poll reads up to 10,000 commits; a larger window can skip its oldest commits. CAUTION: a workflow that writes a file to the watched branch can create a commit that re-triggers itself. Each new commit has a new SHA, so deduplication cannot stop the loop.
|
|
32273
|
-
docs: "Watches commits on the selected branch, or the repository’s default branch when Branch is blank. Starts one run per commit whose committer date is newer than the saved cursor.\n\nThe cursor starts at the time you turn the link on. Push time is not recorded: commits created earlier and pushed later, including existing commits added by a fast-forward merge, can be skipped. Rebasing or squashing can assign new committer dates.\n\nEach poll reads up to 10,000 commits. If more commits match, the oldest may be skipped.\n\nCAUTION: a workflow that writes a file to the watched branch can create a commit that re-triggers itself. Each new commit has a new SHA, so deduplication cannot stop the loop.
|
|
32332
|
+
description: "Starts one run per commit whose committer date is newer than this link’s cursor. This follows commit dates, not push times: pushing an older local commit or merging existing commits without changing their committer dates can leave them undetected. A poll reads up to 10,000 commits; a larger window can skip its oldest commits. CAUTION: a workflow that writes a file to the watched branch can create a commit that re-triggers itself. Each new commit has a new SHA, so deduplication cannot stop the loop. THIS CONNECTOR EXPOSES NO WRITE OPERATION, so the `isOwnEcho` guard has nothing to compare against and the workaround is the branch: write to a separate branch that this link does not watch. A trigger mapping cannot skip an event.",
|
|
32333
|
+
docs: "Watches commits on the selected branch, or the repository’s default branch when Branch is blank. Starts one run per commit whose committer date is newer than the saved cursor.\n\nThe cursor starts at the time you turn the link on. Push time is not recorded: commits created earlier and pushed later, including existing commits added by a fast-forward merge, can be skipped. Rebasing or squashing can assign new committer dates.\n\nEach poll reads up to 10,000 commits. If more commits match, the oldest may be skipped.\n\nCAUTION: a workflow that writes a file to the watched branch can create a commit that re-triggers itself. Each new commit has a new SHA, so deduplication cannot stop the loop. This connector exposes no write operation, so the `isOwnEcho` guard has nothing to compare against, and the workaround is the branch: write to a separate branch that this link does not watch. A trigger mapping cannot skip an event.",
|
|
32274
32334
|
sampleData: {
|
|
32275
32335
|
sha: "11589d462dc2b29714240e03ce310e7b15aa94c3",
|
|
32276
32336
|
url: "https://github.com/stackbone/stackbone/commit/11589d462dc2b29714240e03ce310e7b15aa94c3",
|
|
@@ -32351,8 +32411,8 @@ var init_github_connector = __esmMin((() => {
|
|
|
32351
32411
|
}, {
|
|
32352
32412
|
id: "issue-updated",
|
|
32353
32413
|
displayName: "Issue updated",
|
|
32354
|
-
description: "Fires on every change to an issue in the repository after the link was turned on, following `updated_at`. CLOSED ISSUES ARE INCLUDED — the link asks for `state=all`, so resolving an issue is a change you receive. PULL REQUESTS ARRIVE HERE TOO: GitHub models every pull request as an issue and this endpoint returns both, and only a `pullRequest` object on the payload tells them apart — it is absent on a real issue. A newly opened issue arrives as well, with `createdAt` equal to `updatedAt`. CAUTION: a workflow that writes back to the issue it received — a comment, a label, a close — bumps `updated_at` and re-triggers itself, and the delivery journal cannot stop it. GitHub sends nothing that says who made a change (`author` is who OPENED the issue), so the guard
|
|
32355
|
-
docs: "Starts a run on every change to an issue in the repository, from the moment the link is turned on. Closed issues count: resolving one is a change you receive. Pull requests arrive here too, because GitHub models each one as an issue. A `pullRequest` object on the event is what tells them apart, and it is absent on a real issue. A newly opened issue arrives as well, with `createdAt` equal to `updatedAt`.\n\nCAUTION: a workflow that writes back to the issue it received (a comment, a label, a close) bumps `updatedAt` and starts itself again. Nothing on the platform stops that loop, and GitHub sends no field saying who made a change (`author` is whoever opened the issue).
|
|
32414
|
+
description: "Fires on every change to an issue in the repository after the link was turned on, following `updated_at`. CLOSED ISSUES ARE INCLUDED — the link asks for `state=all`, so resolving an issue is a change you receive. PULL REQUESTS ARRIVE HERE TOO: GitHub models every pull request as an issue and this endpoint returns both, and only a `pullRequest` object on the payload tells them apart — it is absent on a real issue. A newly opened issue arrives as well, with `createdAt` equal to `updatedAt`. CAUTION: a workflow that writes back to the issue it received — a comment, a label, a close — bumps `updated_at` and re-triggers itself, and the delivery journal cannot stop it. GitHub sends nothing that says who made a change (`author` is who OPENED the issue), and THIS CONNECTOR EXPOSES NO WRITE OPERATION, so the `isOwnEcho` guard has nothing to compare against: whatever wrote the issue did not go through here. Until a write lands on this connector the workaround is outside it. A trigger mapping cannot skip an item; every delivered event starts a run.",
|
|
32415
|
+
docs: "Starts a run on every change to an issue in the repository, from the moment the link is turned on. Closed issues count: resolving one is a change you receive. Pull requests arrive here too, because GitHub models each one as an issue. A `pullRequest` object on the event is what tells them apart, and it is absent on a real issue. A newly opened issue arrives as well, with `createdAt` equal to `updatedAt`.\n\nCAUTION: a workflow that writes back to the issue it received (a comment, a label, a close) bumps `updatedAt` and starts itself again. Nothing on the platform stops that loop, and GitHub sends no field saying who made a change (`author` is whoever opened the issue). This connector exposes no write operation, so the `isOwnEcho` guard has nothing to compare against: whatever wrote the issue did not go through here. Until a write lands on this connector, the workaround is outside it. A mapping cannot skip an event: every delivery starts a run.",
|
|
32356
32416
|
sampleData: {
|
|
32357
32417
|
issueId: 5320382826,
|
|
32358
32418
|
number: 14317,
|
|
@@ -32748,7 +32808,7 @@ var init_google_drive_connector = __esmMin((() => {
|
|
|
32748
32808
|
post: {
|
|
32749
32809
|
operationId: "drive.files.create",
|
|
32750
32810
|
summary: "Create a folder, or a file with metadata but no content",
|
|
32751
|
-
description: "Metadata only — this sends JSON, never bytes. A folder is `mimeType: \"application/vnd.google-apps.folder\"`. To create a file WITH content use `drive.files.upload`, which sends the bytes from object storage against Google's upload host; this operation creates the file empty. If a workflow of yours also watches the destination folder, ask for `
|
|
32811
|
+
description: "Metadata only — this sends JSON, never bytes. A folder is `mimeType: \"application/vnd.google-apps.folder\"`. To create a file WITH content use `drive.files.upload`, which sends the bytes from object storage against Google's upload host; this operation creates the file empty. If a workflow of yours also watches the destination folder, ask for `id` in `fields` and declare `echoPath: 'id'` on the call, then open that workflow with `isOwnEcho('google-drive', event.fileId)`. The trigger keys on the file ID, so the ID is the value to compare — a `modifiedTime` echo would never match it.",
|
|
32752
32812
|
"x-stackbone-version-path": "modifiedTime",
|
|
32753
32813
|
parameters: [FIELDS_PARAM, SUPPORTS_ALL_DRIVES_PARAM],
|
|
32754
32814
|
requestBody: {
|
|
@@ -32818,7 +32878,7 @@ var init_google_drive_connector = __esmMin((() => {
|
|
|
32818
32878
|
patch: {
|
|
32819
32879
|
operationId: "drive.files.update",
|
|
32820
32880
|
summary: "Rename a file, move it between folders, or set its metadata",
|
|
32821
|
-
description: "The move is the two QUERY parameters, not the body: `addParents` and `removeParents`. `name` goes in the JSON body. Both in one call is how a file gets named and filed at once.
|
|
32881
|
+
description: "The move is the two QUERY parameters, not the body: `addParents` and `removeParents`. `name` goes in the JSON body. Both in one call is how a file gets named and filed at once. This one does NOT re-trigger `file-added-to-folder`: that trigger fires once per file, so an edit to a file it already delivered starts nothing. The self-write loop to guard is the one the CREATE and UPLOAD operations open, not this one.",
|
|
32822
32882
|
"x-stackbone-version-path": "modifiedTime",
|
|
32823
32883
|
parameters: [
|
|
32824
32884
|
{
|
|
@@ -33126,8 +33186,8 @@ var init_google_drive_connector = __esmMin((() => {
|
|
|
33126
33186
|
triggers: [{
|
|
33127
33187
|
id: "file-added-to-folder",
|
|
33128
33188
|
displayName: "File added to folder",
|
|
33129
|
-
description: "Starts a run the first time a file in one Drive folder arrives or changes — once per file, so later edits to a file already delivered start nothing. Watches the folder itself, not its subfolders. CAUTION: a file this connector uploads to the watched folder can re-trigger the workflow. Each upload creates a new file id, so deduplication cannot stop the loop. Guard
|
|
33130
|
-
docs: "Starts a run the first time a file in one Drive folder arrives or changes. Once per file: a later edit to a file already delivered starts nothing. It watches the folder itself, not its subfolders.\n\nCAUTION: a file this connector uploads to the watched folder
|
|
33189
|
+
description: "Starts a run the first time a file in one Drive folder arrives or changes — once per file, so later edits to a file already delivered start nothing. Watches the folder itself, not its subfolders. CAUTION: a file this connector uploads to the watched folder can re-trigger the workflow. Each upload creates a new file id, so deduplication cannot stop the loop. Guard INSIDE THE WORKFLOW BODY with `isOwnEcho`, for a file the workflow creates by calling `drive.files.create` or `drive.files.upload` ITSELF: pass `fields: 'id'` and `echoPath: 'id'` on that call, then open the workflow with `if (await isOwnEcho('google-drive', event.fileId)) return;`. NAME `fields` FOR WHATEVER THE ECHO PATH POINTS AT, because Drive answers a partial response by default and a path onto a field it left out resolves to nothing, which makes the guard throw rather than answer. THE `drive.file-write` OUTPUT WRITER CANNOT CARRY AN ECHO PATH, so a workflow that files its output through it is not covered and still needs separate input and output folders. A trigger mapping cannot skip an event.",
|
|
33190
|
+
docs: "Starts a run the first time a file in one Drive folder arrives or changes. Once per file: a later edit to a file already delivered starts nothing. It watches the folder itself, not its subfolders.\n\nCAUTION: a file this connector uploads to the watched folder starts the workflow again. Each upload creates a new file id, so deduplication cannot stop the loop. Guard inside the workflow body with `isOwnEcho`, for a file the workflow creates by calling Drive's own `create` or `upload` operation itself: pass `fields: 'id'` and `echoPath: 'id'` on that call, then open the workflow with `if (await isOwnEcho('google-drive', event.fileId)) return;`. Name `fields` for whatever the echo path points at, because Drive answers a partial response by default, and a path onto a field it left out resolves to nothing, which makes the guard throw rather than answer. The `drive.file-write` output writer cannot carry an echo path, so a workflow that files its output through it is not covered and still needs separate input and output folders. A mapping cannot skip an event: every delivery starts a run.",
|
|
33131
33191
|
sampleData: {
|
|
33132
33192
|
fileId: "1a2B3cD4eF5gH6iJ7kL8mN9oP",
|
|
33133
33193
|
name: "Weekly sync 2026-08-25.pdf",
|
|
@@ -33347,8 +33407,8 @@ var init_linear_connector = __esmMin((() => {
|
|
|
33347
33407
|
{
|
|
33348
33408
|
id: "issue-created",
|
|
33349
33409
|
displayName: "Issue created",
|
|
33350
|
-
description: "Fires once per issue created in the workspace after the link was turned on, following `createdAt`. CAUTION: a workflow that creates an issue in this workspace can re-trigger itself. Each new issue has a new id, so deduplication cannot stop the loop. Guard
|
|
33351
|
-
docs: "Starts one run per issue created in the workspace, from the moment the link is turned on.\n\nCAUTION: a workflow that creates an issue in this workspace
|
|
33410
|
+
description: "Fires once per issue created in the workspace after the link was turned on, following `createdAt`. CAUTION: a workflow that creates an issue in this workspace can re-trigger itself. Each new issue has a new id, so deduplication cannot stop the loop. Guard INSIDE THE WORKFLOW BODY with `isOwnEcho`: have the `issueCreate` mutation select `issue { id }`, declare `echoPath: 'data.issueCreate.issue.id'` on the write, then open the workflow with `if (await isOwnEcho('linear', event.issueId)) return;`. It compares the id your own write returned against the one delivered. The connector id you pass is the one you passed to `connection()`. A trigger mapping cannot skip an event.",
|
|
33411
|
+
docs: "Starts one run per issue created in the workspace, from the moment the link is turned on.\n\nCAUTION: a workflow that creates an issue in this workspace starts itself again. Each new issue has a new id, so deduplication cannot stop the loop. Guard inside the workflow body with `isOwnEcho`. The `issueCreate` mutation has to select `issue { id }`, because the guard can only record a value the response carries. Declare `echoPath: 'data.issueCreate.issue.id'` on the write, then open the workflow with `if (await isOwnEcho('linear', event.issueId)) return;`. That compares the id your own write returned against the one you were handed. The connector id you pass to `isOwnEcho` is the same one you passed to `connection()`. A mapping cannot skip an event: every delivery starts a run.",
|
|
33352
33412
|
sampleData: {
|
|
33353
33413
|
issueId: "a1b2c3d4-0000-0000-0000-000000000000",
|
|
33354
33414
|
identifier: "PLA-812",
|
|
@@ -33437,8 +33497,8 @@ var init_linear_connector = __esmMin((() => {
|
|
|
33437
33497
|
{
|
|
33438
33498
|
id: "comment-created",
|
|
33439
33499
|
displayName: "Comment created",
|
|
33440
|
-
description: "Fires once per comment posted on any issue in the workspace after the link was turned on, following `createdAt`. CAUTION: a workflow that replies by posting a comment in this workspace can re-trigger itself. Each new comment has a new id, so deduplication cannot stop the loop. Guard
|
|
33441
|
-
docs: "Starts one run per comment posted on any issue in the workspace, from the moment the link is turned on.\n\nCAUTION: a workflow that replies by posting a comment in this workspace
|
|
33500
|
+
description: "Fires once per comment posted on any issue in the workspace after the link was turned on, following `createdAt`. CAUTION: a workflow that replies by posting a comment in this workspace can re-trigger itself. Each new comment has a new id, so deduplication cannot stop the loop. Guard INSIDE THE WORKFLOW BODY with `isOwnEcho`: have the `commentCreate` mutation select `comment { id }`, declare `echoPath: 'data.commentCreate.comment.id'` on the write, then open the workflow with `if (await isOwnEcho('linear', event.commentId)) return;`. It compares the id your own write returned against the one delivered. A trigger mapping cannot skip an event.",
|
|
33501
|
+
docs: "Starts one run per comment posted on any issue in the workspace, from the moment the link is turned on.\n\nCAUTION: a workflow that replies by posting a comment in this workspace starts itself again. Each new comment has a new id, so deduplication cannot stop the loop. Guard inside the workflow body with `isOwnEcho`. The `commentCreate` mutation has to select `comment { id }`, because the guard can only record a value the response carries. Declare `echoPath: 'data.commentCreate.comment.id'` on the write, then open the workflow with `if (await isOwnEcho('linear', event.commentId)) return;`. That compares the id your own write returned against the one you were handed. A mapping cannot skip an event: every delivery starts a run.",
|
|
33442
33502
|
sampleData: {
|
|
33443
33503
|
commentId: "e5f6a7b8-0000-0000-0000-000000000000",
|
|
33444
33504
|
body: "Shipped on `next`, closing.",
|
|
@@ -33530,8 +33590,8 @@ var init_linear_connector = __esmMin((() => {
|
|
|
33530
33590
|
{
|
|
33531
33591
|
id: "issue-updated",
|
|
33532
33592
|
displayName: "Issue updated",
|
|
33533
|
-
description: "Fires on every change to an issue in the workspace after the link was turned on, following `updatedAt`. A newly created issue arrives here too — compare `createdAt` with `updatedAt` to tell a birth from an edit. `lastActor` / `lastBotActor` carry the actor of the issue's NEWEST HISTORY ENTRY — a `User` when a personal API key made the change, a bot when an OAuth app did.
|
|
33534
|
-
docs: "Starts a run on every change to an issue in the workspace, from the moment the link is turned on. A newly created issue arrives here too, so compare `createdAt` with `updatedAt` to tell a birth from an edit. Archiving is not delivered, because an archived issue stops matching.\n\n`lastActor` and `lastBotActor` carry whoever made the newest change: `lastActor` when a personal API key did it, `lastBotActor` when an OAuth app did.
|
|
33593
|
+
description: "Fires on every change to an issue in the workspace after the link was turned on, following `updatedAt`. A newly created issue arrives here too — compare `createdAt` with `updatedAt` to tell a birth from an edit. `lastActor` / `lastBotActor` carry the actor of the issue's NEWEST HISTORY ENTRY — a `User` when a personal API key made the change, a bot when an OAuth app did. Use them to answer who touched this, for instance to ignore every bot's edits; they are not a self-write guard, because either can arrive null and an `updatedAt` bump that minted no history entry leaves the previous change's actor in place. CAUTION: a workflow that writes back to the issue it received bumps `updatedAt` and re-triggers itself, and the delivery journal cannot stop it. Guard INSIDE THE WORKFLOW BODY with `isOwnEcho`: declare `echoPath: 'data.issueUpdate.issue.updatedAt'` on the write, then open the workflow with `if (await isOwnEcho('linear', event.updatedAt)) return;`. It compares the value your own write returned against the one delivered, so it does not depend on an actor field being fresh. A trigger mapping cannot skip an item; every delivered event starts a run. Write-back can also overwrite a human edit made since the event was read: issueUpdate exposes no version or expected-value precondition. Write only fields reserved for the workflow; reading again before writing does not make the update atomic. Archiving is not delivered, because an archived issue stops matching.",
|
|
33594
|
+
docs: "Starts a run on every change to an issue in the workspace, from the moment the link is turned on. A newly created issue arrives here too, so compare `createdAt` with `updatedAt` to tell a birth from an edit. Archiving is not delivered, because an archived issue stops matching.\n\n`lastActor` and `lastBotActor` carry whoever made the newest change: `lastActor` when a personal API key did it, `lastBotActor` when an OAuth app did. They answer who touched this, which is useful on its own, for instance to ignore edits made by bots. They are not the self-write guard: either can arrive null, and a change that minted no history entry leaves the previous actor in place.\n\nCAUTION: a workflow that writes back to the issue it received bumps `updatedAt` and starts itself again. Nothing on the platform stops that loop. Guard inside the workflow body with `isOwnEcho`. Declare where your write reports the new version, `echoPath: 'data.issueUpdate.issue.updatedAt'`, then open the workflow with `if (await isOwnEcho('linear', event.updatedAt)) return;`. That compares the value your own write returned against the one you were handed, so it holds whether or not an actor field is fresh. A mapping cannot skip an event: every delivery starts a run.\n\nWrite-back can also overwrite a human edit made since the event was read. `issueUpdate` exposes no version or expected-value precondition. Write only fields reserved for the workflow; reading again before writing does not make the update atomic.",
|
|
33535
33595
|
sampleData: {
|
|
33536
33596
|
issueId: "a1b2c3d4-0000-0000-0000-000000000000",
|
|
33537
33597
|
identifier: "PLA-812",
|
|
@@ -33944,14 +34004,15 @@ var init_supabase_connector = __esmMin((() => {
|
|
|
33944
34004
|
"42"
|
|
33945
34005
|
],
|
|
33946
34006
|
accessDenied: ["42501"],
|
|
33947
|
-
conflict: ["23"]
|
|
34007
|
+
conflict: ["23"],
|
|
34008
|
+
cardinalityMismatch: ["PGRST124"]
|
|
33948
34009
|
},
|
|
33949
34010
|
scopes: [],
|
|
33950
34011
|
triggers: [{
|
|
33951
34012
|
id: "row-inserted",
|
|
33952
34013
|
displayName: "Row inserted or changed",
|
|
33953
|
-
description: "Starts a run for each row that appears past the cursor column, delivered as { table, row }. THE CURSOR COLUMN DECIDES WHAT \"APPEARS\" MEANS. Left at the default `id`, or set to any column written once and never touched again such as `created_at`, this is insert-only: one run per new row, and an edit to an existing row is never delivered. Set to a column your database bumps on every write, such as `updated_at`, this is change detection: EVERY UPDATE TO A ROW STARTS A NEW RUN. CAUTION: on a moving column a
|
|
33954
|
-
docs: "Starts a run for each row that appears past the cursor column, delivered as `{ table, row }`.\n\nThe cursor column decides what \"appears\" means, and it is the only choice that matters here. Left at the default `id`, or set to a column written once and never touched again such as `created_at`, this is insert-only: one run per new row, and an edit to an existing row is never delivered. Set to a column your database bumps on every write, such as `updated_at`, it becomes change detection, and every update to a row starts a new run.\n\nThe table must expose a unique, non-null `id` column for delivery identity. Rows whose cursor value is NULL are ignored.\n\nRows are read in ascending cursor order. A row that becomes visible after the cursor advances, with a value equal to or lower than the highest already seen, is not delivered. Choose values that increase as changes become visible; this also applies to timestamp and date columns.\n\nCAUTION: on a moving column, a workflow that
|
|
34014
|
+
description: "Starts a run for each row that appears past the cursor column, delivered as { table, row }. THE CURSOR COLUMN DECIDES WHAT \"APPEARS\" MEANS. Left at the default `id`, or set to any column written once and never touched again such as `created_at`, this is insert-only: one run per new row, and an edit to an existing row is never delivered. Set to a column your database bumps on every write, such as `updated_at`, this is change detection: EVERY UPDATE TO A ROW STARTS A NEW RUN. CAUTION: a workflow that writes to this table can re-trigger itself, and the delivery journal cannot stop it: on a moving column a write-back bumps that column, and on the default column a workflow that answers each row by inserting another mints a new id. Guard INSIDE THE WORKFLOW BODY with `isOwnEcho`, and MATCH THE VALUE TO THE CURSOR COLUMN. Insert-only, compare the new row's id: `echoPath: '0.id'` on the write against `event.row.id`. Change detection, compare the cursor column itself: `echoPath: '0.updated_at'` against `event.row.updated_at`, with your own column name in place of `updated_at`. COMPARING THE ID ON A MOVING COLUMN IS WRONG — a row's id never changes, so the guard would read every later delivery of that row as your own echo for the whole window and swallow a human's edit with it. The write must also ask for `Prefer: return=representation`: without it PostgREST answers no body, the echo path resolves to nothing, and the guard throws instead of guarding. The table must expose a unique, non-null `id` column for delivery identity. Rows whose cursor value is NULL are ignored. Rows are read in ascending cursor order: a row that becomes visible after the cursor advances, with a value EQUAL TO OR LOWER THAN the highest seen, is not delivered. Choose values that increase as changes become visible; this also applies to timestamp and date columns. A trigger mapping cannot skip an item; every delivered row starts a run.",
|
|
34015
|
+
docs: "Starts a run for each row that appears past the cursor column, delivered as `{ table, row }`.\n\nThe cursor column decides what \"appears\" means, and it is the only choice that matters here. Left at the default `id`, or set to a column written once and never touched again such as `created_at`, this is insert-only: one run per new row, and an edit to an existing row is never delivered. Set to a column your database bumps on every write, such as `updated_at`, it becomes change detection, and every update to a row starts a new run.\n\nThe table must expose a unique, non-null `id` column for delivery identity. Rows whose cursor value is NULL are ignored.\n\nRows are read in ascending cursor order. A row that becomes visible after the cursor advances, with a value equal to or lower than the highest already seen, is not delivered. Choose values that increase as changes become visible; this also applies to timestamp and date columns.\n\nCAUTION: a workflow that writes to this table starts itself again, and nothing on the platform stops that loop: on a moving column a write-back bumps that column, and on the default column a workflow that answers each row by inserting another row mints a new id. Guard inside the workflow body with `isOwnEcho`, and match the value you compare to the cursor column. Insert-only: compare the `id` the delivery carried, with `echoPath: '0.id'` on the write. Change detection: compare the cursor column the delivery carried, with `echoPath: '0.updated_at'` on the write, putting your own column name in place of `updated_at`. Comparing the id on a moving column is wrong, because a row's id never changes, so the guard would read every later delivery of that row as your own echo for the whole window and swallow a human's edit with it. The write also has to ask for `Prefer: return=representation`: without it PostgREST answers no body, the echo path resolves to nothing, and the guard throws instead of guarding. A mapping cannot skip an event: every delivered row starts a run.",
|
|
33955
34016
|
sampleData: {
|
|
33956
34017
|
table: "todos",
|
|
33957
34018
|
row: {
|
|
@@ -34465,6 +34526,7 @@ function buildOperation(rawPath, resolvedPath, method, operation, pathLevelParam
|
|
|
34465
34526
|
const parameters = [];
|
|
34466
34527
|
const seen = /* @__PURE__ */ new Set();
|
|
34467
34528
|
let bodyParamSchema;
|
|
34529
|
+
let bodyArgName;
|
|
34468
34530
|
for (let i = rawParameters.length - 1; i >= 0; i--) {
|
|
34469
34531
|
const raw = derefParameter(rawParameters[i], root);
|
|
34470
34532
|
if (raw === void 0) {
|
|
@@ -34474,7 +34536,10 @@ function buildOperation(rawPath, resolvedPath, method, operation, pathLevelParam
|
|
|
34474
34536
|
if (isRecord$2(raw) && source === "swagger") {
|
|
34475
34537
|
const location = asString$1(raw["in"]);
|
|
34476
34538
|
if (location === "body") {
|
|
34477
|
-
if (bodyParamSchema === void 0 && raw["schema"] !== void 0)
|
|
34539
|
+
if (bodyParamSchema === void 0 && raw["schema"] !== void 0) {
|
|
34540
|
+
bodyParamSchema = raw["schema"];
|
|
34541
|
+
bodyArgName = asString$1(raw["name"]);
|
|
34542
|
+
}
|
|
34478
34543
|
continue;
|
|
34479
34544
|
}
|
|
34480
34545
|
if (location === "formData") {
|
|
@@ -34507,6 +34572,7 @@ function buildOperation(rawPath, resolvedPath, method, operation, pathLevelParam
|
|
|
34507
34572
|
...description !== void 0 ? { description } : {},
|
|
34508
34573
|
...parameters.length > 0 ? { parameters } : {},
|
|
34509
34574
|
...requestBodySchema !== void 0 ? { requestBodySchema } : {},
|
|
34575
|
+
...bodyArgName !== void 0 ? { bodyArgName } : {},
|
|
34510
34576
|
...responseSchema !== void 0 ? { responseSchema } : {},
|
|
34511
34577
|
...rawBody ? { rawBody } : {},
|
|
34512
34578
|
...rawResponse !== void 0 ? { rawResponse } : {},
|
|
@@ -36148,17 +36214,18 @@ function narrows(value, narrowingOperators) {
|
|
|
36148
36214
|
return !(operator === "is" && NEGATED_IS.has(rest.toLowerCase()));
|
|
36149
36215
|
}
|
|
36150
36216
|
/**
|
|
36151
|
-
* Should this call be refused as an unfiltered
|
|
36217
|
+
* Should this call be refused as an unfiltered DELETE or PATCH?
|
|
36152
36218
|
*
|
|
36153
36219
|
* 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.
|
|
36220
|
+
* would narrow it — or `null` when the call may proceed. DELETE and PATCH
|
|
36221
|
+
* both affect existing rows. Reads and inserts keep their existing behavior.
|
|
36222
|
+
* Only declared query arguments count; values inside the named body do not.
|
|
36158
36223
|
*/
|
|
36159
36224
|
function unfilteredDeleteError(def, operation, args) {
|
|
36160
36225
|
const guard = def.unfilteredDeleteGuard;
|
|
36161
|
-
|
|
36226
|
+
const method = operation.method.toUpperCase();
|
|
36227
|
+
if (guard === void 0 || method !== "DELETE" && method !== "PATCH") return null;
|
|
36228
|
+
const effect = method === "PATCH" ? "update" : "remove";
|
|
36162
36229
|
const nonFilter = new Set(guard.nonFilterParams);
|
|
36163
36230
|
const filterParams = (operation.parameters ?? []).filter((param) => param.in === "query" && !nonFilter.has(param.name)).map((param) => param.name);
|
|
36164
36231
|
const present = filterParams.filter((name) => {
|
|
@@ -36169,9 +36236,9 @@ function unfilteredDeleteError(def, operation, args) {
|
|
|
36169
36236
|
if (narrowingOperators === void 0) {
|
|
36170
36237
|
if (present.length > 0) return null;
|
|
36171
36238
|
} 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,
|
|
36239
|
+
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(", ")}.`);
|
|
36240
|
+
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.");
|
|
36241
|
+
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
36242
|
}
|
|
36176
36243
|
/** The one refusal shape, so every path names the operation the same way. */
|
|
36177
36244
|
function refusal(operation, reason, remedy) {
|
|
@@ -36261,6 +36328,7 @@ function classifyProviderError(def, status, body, operationId) {
|
|
|
36261
36328
|
if (matched !== null && provider !== null) {
|
|
36262
36329
|
const said = providerSaid(provider);
|
|
36263
36330
|
switch (matched) {
|
|
36331
|
+
case "cardinalityMismatch": return makeBrokerError("cardinality_mismatch", `operation "${operationId}" exceeded the provider's requested row limit: ${said}`);
|
|
36264
36332
|
case "invalidArgs": return makeBrokerError("invalid_args", `operation "${operationId}" was refused by the provider as a bad request: ${said}`);
|
|
36265
36333
|
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
36334
|
case "conflict": return makeBrokerError("execute_failed", `operation "${operationId}" hit a constraint conflict: ${said}`, false);
|
|
@@ -36295,7 +36363,8 @@ function longestMatch(classes, code) {
|
|
|
36295
36363
|
const lists = [
|
|
36296
36364
|
["invalidArgs", classes.invalidArgs],
|
|
36297
36365
|
["accessDenied", classes.accessDenied],
|
|
36298
|
-
["conflict", classes.conflict]
|
|
36366
|
+
["conflict", classes.conflict],
|
|
36367
|
+
["cardinalityMismatch", classes.cardinalityMismatch ?? []]
|
|
36299
36368
|
];
|
|
36300
36369
|
for (const [kind, prefixes] of lists) for (const prefix of prefixes) if (code.startsWith(prefix) && (best === null || prefix.length > best.prefix.length)) best = {
|
|
36301
36370
|
prefix,
|
|
@@ -36578,14 +36647,66 @@ async function runOperation(def, operation, args, token, http, timeoutMs, opts =
|
|
|
36578
36647
|
headerValue: responseHeader(response, continuation.urlHeader)
|
|
36579
36648
|
}) : void 0;
|
|
36580
36649
|
const output = response.ok && wantsRawResponse(operation, args) ? await storeRawResponse(operation, args, response, opts) : await parseResponse(response);
|
|
36650
|
+
const matchedRows = matchedRowTotal(response);
|
|
36581
36651
|
return {
|
|
36582
36652
|
status: response.status,
|
|
36583
36653
|
ok: response.ok,
|
|
36584
36654
|
output,
|
|
36655
|
+
...matchedRows !== void 0 ? { matchedRows } : {},
|
|
36585
36656
|
...continuationUrl !== void 0 ? { continuationUrl } : {}
|
|
36586
36657
|
};
|
|
36587
36658
|
}
|
|
36588
36659
|
/**
|
|
36660
|
+
* The TOTAL out of a `Content-Range` response header — how many rows the call
|
|
36661
|
+
* matched — or `undefined` when the provider did not count.
|
|
36662
|
+
*
|
|
36663
|
+
* `Content-Range` is `<first>-<last>/<total>`, and either half may be `*`. The
|
|
36664
|
+
* range says which rows came back in THIS response and the total says how many
|
|
36665
|
+
* exist; only the total answers "did my filter hit anything", because a write
|
|
36666
|
+
* that asks for no representation returns no rows either way. Measured against
|
|
36667
|
+
* PostgREST v12.2.3: a matched update answers `0-0/1`, one that matched nothing
|
|
36668
|
+
* answers a star range over a zero total, and one that asked for no count
|
|
36669
|
+
* answers a star over a star (`docs/investigations/pla-1345/README-live.md`).
|
|
36670
|
+
*
|
|
36671
|
+
* A `*` total is dropped rather than read as zero. That distinction is the guard
|
|
36672
|
+
* on the whole feature: see `matchedRows`.
|
|
36673
|
+
*/
|
|
36674
|
+
function matchedRowTotal(response) {
|
|
36675
|
+
const total = responseHeader(response, "Content-Range")?.split("/")[1];
|
|
36676
|
+
if (total === void 0 || total === "*") return void 0;
|
|
36677
|
+
const parsed = Number(total);
|
|
36678
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : void 0;
|
|
36679
|
+
}
|
|
36680
|
+
/**
|
|
36681
|
+
* Did a write carrying a precondition match nothing (PLA-1403)?
|
|
36682
|
+
*
|
|
36683
|
+
* Returns the terminal refusal, or `null` when the call may be reported as the
|
|
36684
|
+
* success it looks like. A conditional write sends the value it read as part of
|
|
36685
|
+
* the row filter, so a row somebody else moved matches nothing and is left
|
|
36686
|
+
* alone — the protection working. Without this the caller cannot tell, because
|
|
36687
|
+
* `execute` returns `{ ok: true, output }` and the count never leaves the
|
|
36688
|
+
* broker.
|
|
36689
|
+
*
|
|
36690
|
+
* TWO conditions, and both are load-bearing:
|
|
36691
|
+
*
|
|
36692
|
+
* * The method changes EXISTING rows: `PATCH` and `DELETE`, the same pair
|
|
36693
|
+
* `unfilteredDeleteError` guards. A `GET` that matches nothing is a correct
|
|
36694
|
+
* answer to a question and must stay a success — the one case this must
|
|
36695
|
+
* never break. `POST` is out because an insert matches nothing by
|
|
36696
|
+
* definition, and `PUT` is out because PostgREST upserts on it: a PUT that
|
|
36697
|
+
* matches nothing CREATES the row, so zero matched is not a refusal there.
|
|
36698
|
+
* Unmeasured on our side, which is the second reason to leave it out.
|
|
36699
|
+
* * The provider reported a total of exactly zero, which it only does when the
|
|
36700
|
+
* caller asked for the count. Asking IS the declaration, so a write that did
|
|
36701
|
+
* not ask keeps the behaviour it has always had. An empty body is NOT the
|
|
36702
|
+
* trigger.
|
|
36703
|
+
*/
|
|
36704
|
+
function unmatchedWriteError(operation, matchedRows, operationId) {
|
|
36705
|
+
const method = operation.method.toUpperCase();
|
|
36706
|
+
if (matchedRows !== 0 || method !== "PATCH" && method !== "DELETE") return null;
|
|
36707
|
+
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.`);
|
|
36708
|
+
}
|
|
36709
|
+
/**
|
|
36589
36710
|
* Render a parsed operation-call body to a short single-line string for an error
|
|
36590
36711
|
* message. Shared by the action executor (here) and the inbound-trigger poll
|
|
36591
36712
|
* context, so the two never drift. `empty` is returned for null/undefined (the
|
|
@@ -36599,13 +36720,36 @@ function formatOperationBody(output, opts = {}) {
|
|
|
36599
36720
|
return max > 0 && text.length > max ? `${text.slice(0, max)}…` : text;
|
|
36600
36721
|
}
|
|
36601
36722
|
/**
|
|
36723
|
+
* Fallback classification after provider-specific body codes (PLA-1321).
|
|
36724
|
+
* Known input, access and resource refusals are terminal. Unclassified statuses
|
|
36725
|
+
* retain the existing unknown-outcome policy, including 409: conflict vocabulary
|
|
36726
|
+
* is outside this slice. The caller's idempotency option still governs that path.
|
|
36727
|
+
*/
|
|
36728
|
+
function brokerCodeForProviderStatus(status) {
|
|
36729
|
+
switch (status) {
|
|
36730
|
+
case 400:
|
|
36731
|
+
case 405:
|
|
36732
|
+
case 422: return "invalid_args";
|
|
36733
|
+
case 403: return "access_denied";
|
|
36734
|
+
case 404:
|
|
36735
|
+
case 410: return "not_found";
|
|
36736
|
+
default: return "execute_failed";
|
|
36737
|
+
}
|
|
36738
|
+
}
|
|
36739
|
+
/**
|
|
36602
36740
|
* Build the JSON request body for an OpenAPI operation. When the operation
|
|
36603
|
-
*
|
|
36741
|
+
* names a Swagger body argument containing an object, send that object directly.
|
|
36742
|
+
* Otherwise, every payload key that is NOT a declared path/query/
|
|
36604
36743
|
* header param becomes the body. Sets `content-type` when a body is produced.
|
|
36605
36744
|
* Returns `undefined` for body-less operations (e.g. GET).
|
|
36606
36745
|
*/
|
|
36607
36746
|
function buildRequestBody(operation, args, headers) {
|
|
36608
36747
|
if (operation.requestBodySchema === void 0) return void 0;
|
|
36748
|
+
const namedBody = operation.bodyArgName === void 0 ? void 0 : args[operation.bodyArgName];
|
|
36749
|
+
if (namedBody !== null && typeof namedBody === "object") {
|
|
36750
|
+
headers["content-type"] = "application/json";
|
|
36751
|
+
return JSON.stringify(namedBody);
|
|
36752
|
+
}
|
|
36609
36753
|
const consumed = /* @__PURE__ */ new Set();
|
|
36610
36754
|
for (const param of operation.parameters ?? []) if (param.in === "path" || param.in === "query" || param.in === "header") consumed.add(param.name);
|
|
36611
36755
|
for (const arg of brokerConsumedArgs(operation)) consumed.add(arg.name);
|
|
@@ -36786,6 +36930,41 @@ function fail(error) {
|
|
|
36786
36930
|
error
|
|
36787
36931
|
};
|
|
36788
36932
|
}
|
|
36933
|
+
/**
|
|
36934
|
+
* A PROVIDER ROUND-TRIP that failed with an outcome nobody here can determine,
|
|
36935
|
+
* classified against the caller's own idempotency assertion (PLA-1349).
|
|
36936
|
+
*
|
|
36937
|
+
* `execute_failed` and `timeout` are the two retryable codes in
|
|
36938
|
+
* `BROKER_ERROR_RETRYABILITY`, and that default is right: a transport failure or
|
|
36939
|
+
* a gateway 5xx usually did not land. But they are also the only two where it
|
|
36940
|
+
* MIGHT have — the provider committed and the response was lost, or the deadline
|
|
36941
|
+
* fired after the write was accepted and the abort only closed the socket. A
|
|
36942
|
+
* durable step is retried whole, so retrying one of these re-sends the write and
|
|
36943
|
+
* one run creates two objects.
|
|
36944
|
+
*
|
|
36945
|
+
* Which of the two it is cannot be read off the code, the status or the connector
|
|
36946
|
+
* definition: `commentCreate` and `listComments` fail identically, and an
|
|
36947
|
+
* operation the provider deduplicates only deduplicates when the CALLER minted
|
|
36948
|
+
* the key. So the caller states it and this decides accordingly:
|
|
36949
|
+
*
|
|
36950
|
+
* - `false` → `retryable: false`. The SDK turns that into `fatal: true`, the
|
|
36951
|
+
* engine stops the step on its first failure, and a human reconciles. Same
|
|
36952
|
+
* trade the World already made for a dispatch past `httpTimeoutMs`.
|
|
36953
|
+
* - `true`, or NOT STATED → the code's own default, untouched. Passing
|
|
36954
|
+
* `undefined` as the override is byte-identical to omitting the argument, which
|
|
36955
|
+
* is what keeps an unstated option a true no-op.
|
|
36956
|
+
*
|
|
36957
|
+
* ONLY these two codes go through here. A code that was already terminal stays
|
|
36958
|
+
* terminal with no second decision, and a retryable code whose outcome is KNOWN —
|
|
36959
|
+
* a 401 the provider refused before doing anything, a stale-spec refusal, a 429
|
|
36960
|
+
* a rate limiter answered in front of the operation, a 408 for a request the
|
|
36961
|
+
* provider never received whole — keeps its retry, because there is nothing to
|
|
36962
|
+
* reconcile. Callers pass `undefined` for those rather than the assertion; see
|
|
36963
|
+
* the provider non-2xx exit.
|
|
36964
|
+
*/
|
|
36965
|
+
function unknownOutcome(code, message, idempotent) {
|
|
36966
|
+
return makeBrokerError(code, message, idempotent === false ? false : void 0);
|
|
36967
|
+
}
|
|
36789
36968
|
function messageOf$6(err) {
|
|
36790
36969
|
return err instanceof Error ? err.message : String(err);
|
|
36791
36970
|
}
|
|
@@ -36840,7 +37019,7 @@ var init_connect_action_executor = __esmMin((() => {
|
|
|
36840
37019
|
this.resumableStallTimeoutMs = deps.resumableStallTimeoutMs ?? 45e3;
|
|
36841
37020
|
}
|
|
36842
37021
|
async execute(input) {
|
|
36843
|
-
const { orgId, runId, connectConnectorId, connectOperationId, payload } = input;
|
|
37022
|
+
const { orgId, runId, connectConnectorId, connectOperationId, payload, idempotent } = input;
|
|
36844
37023
|
const runDef = runId !== void 0 ? await this.deps.runConnectors?.resolve(runId, orgId, connectConnectorId) ?? null : null;
|
|
36845
37024
|
const record = await this.deps.connectorRows?.resolve(orgId, connectConnectorId) ?? null;
|
|
36846
37025
|
let def;
|
|
@@ -36903,13 +37082,13 @@ var init_connect_action_executor = __esmMin((() => {
|
|
|
36903
37082
|
if (followUp === void 0) return this.missingFromSpec(specEntry, cached, `operation "${connectOperationId}" continues as "${declared.operationId}", which is not on connector "${connectConnectorId}"`);
|
|
36904
37083
|
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
37084
|
}
|
|
36906
|
-
return this.runOpenApi(def, operation, payload, tokenResponse, caller, specEntry, cached, followUp);
|
|
37085
|
+
return this.runOpenApi(def, operation, payload, tokenResponse, caller, specEntry, cached, followUp, idempotent);
|
|
36907
37086
|
}
|
|
36908
37087
|
const tool = findTool(introspection.tools, connectOperationId);
|
|
36909
37088
|
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);
|
|
37089
|
+
return this.runMcp(def, tool, payload, tokenResponse, caller, idempotent);
|
|
36911
37090
|
}
|
|
36912
|
-
async runOpenApi(def, operation, payload, token, caller, specEntry, cached, followUp) {
|
|
37091
|
+
async runOpenApi(def, operation, payload, token, caller, specEntry, cached, followUp, idempotent) {
|
|
36913
37092
|
const args = isRecord$1(payload) ? payload : {};
|
|
36914
37093
|
for (const phase of followUp === void 0 ? [operation] : [operation, followUp]) {
|
|
36915
37094
|
const refused = unfilteredDeleteError(def, phase, args);
|
|
@@ -36933,11 +37112,12 @@ var init_connect_action_executor = __esmMin((() => {
|
|
|
36933
37112
|
});
|
|
36934
37113
|
}
|
|
36935
37114
|
} 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(
|
|
37115
|
+
if (err instanceof ActionTimeoutError) return fail(unknownOutcome("timeout", `operation "${reported.operationId}" timed out`, idempotent));
|
|
37116
|
+
if (isTransferStalled(err)) return fail(unknownOutcome("timeout", `operation "${reported.operationId}" stalled mid-transfer: ${messageOf$6(err)}`, idempotent));
|
|
37117
|
+
if (err instanceof ContinuationError) return fail(makeBrokerError(err.code, err.message, idempotent === false ? false : err.retryable));
|
|
37118
|
+
if (err instanceof RawResponseError) return fail(makeBrokerError(err.code, err.message, idempotent === false ? false : void 0));
|
|
37119
|
+
if (err instanceof RawBodyError) return fail(makeBrokerError(err.code, err.message));
|
|
37120
|
+
return fail(unknownOutcome("execute_failed", messageOf$6(err), idempotent));
|
|
36941
37121
|
}
|
|
36942
37122
|
const classified = classifyProviderError(def, result.status, result.output, reported.operationId);
|
|
36943
37123
|
if (classified?.code === "access_denied") return fail(classified);
|
|
@@ -36948,8 +37128,13 @@ var init_connect_action_executor = __esmMin((() => {
|
|
|
36948
37128
|
const body = formatOperationBody(result.output);
|
|
36949
37129
|
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
37130
|
if (classified !== null) return fail(classified);
|
|
36951
|
-
|
|
37131
|
+
const code = brokerCodeForProviderStatus(result.status);
|
|
37132
|
+
const message = `operation "${reported.operationId}" failed with status ${result.status}${body ? `: ${body}` : ""}`;
|
|
37133
|
+
if (code !== "execute_failed") return fail(makeBrokerError(code, message));
|
|
37134
|
+
return fail(unknownOutcome("execute_failed", message, result.status === 429 || result.status === 408 ? void 0 : idempotent));
|
|
36952
37135
|
}
|
|
37136
|
+
const unmatched = unmatchedWriteError(operation, result.matchedRows, reported.operationId);
|
|
37137
|
+
if (unmatched !== null) return fail(unmatched);
|
|
36953
37138
|
return {
|
|
36954
37139
|
ok: true,
|
|
36955
37140
|
output: result.output
|
|
@@ -36974,7 +37159,7 @@ var init_connect_action_executor = __esmMin((() => {
|
|
|
36974
37159
|
...this.deps.maxRawResponseBytes !== void 0 ? { maxRawResponseBytes: this.deps.maxRawResponseBytes } : {}
|
|
36975
37160
|
};
|
|
36976
37161
|
}
|
|
36977
|
-
async runMcp(def, tool, payload, token, caller) {
|
|
37162
|
+
async runMcp(def, tool, payload, token, caller, idempotent) {
|
|
36978
37163
|
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
37164
|
let output;
|
|
36980
37165
|
try {
|
|
@@ -36985,9 +37170,9 @@ var init_connect_action_executor = __esmMin((() => {
|
|
|
36985
37170
|
token: token.token
|
|
36986
37171
|
}), this.timeoutMs);
|
|
36987
37172
|
} catch (err) {
|
|
36988
|
-
if (err instanceof ActionTimeoutError) return fail(
|
|
37173
|
+
if (err instanceof ActionTimeoutError) return fail(unknownOutcome("timeout", `tool "${tool.name}" timed out`, idempotent));
|
|
36989
37174
|
if (statusOf(err) === 401) return this.on401(def, caller, `tool "${tool.name}" returned 401`);
|
|
36990
|
-
return fail(
|
|
37175
|
+
return fail(unknownOutcome("execute_failed", messageOf$6(err), idempotent));
|
|
36991
37176
|
}
|
|
36992
37177
|
return {
|
|
36993
37178
|
ok: true,
|
|
@@ -51649,8 +51834,27 @@ var CONNECT_DDL = `
|
|
|
51649
51834
|
* Ensure the connect tables exist in the emulator Postgres. Idempotent. Exported
|
|
51650
51835
|
* so the workspace-server can run it once at boot (and the spec can run it before
|
|
51651
51836
|
* exercising the routes) instead of paying the DDL on every request.
|
|
51652
|
-
|
|
51653
|
-
|
|
51837
|
+
*
|
|
51838
|
+
* A NO-OP WHEN THE CONTROL PLANE OWNS THE SCHEMA (PLA-1286). In the self-host
|
|
51839
|
+
* topology the box and the control plane share ONE Postgres, and both declare
|
|
51840
|
+
* `connection_grant`: db-core's `0008_connect.sql` with a bare `CREATE TABLE`,
|
|
51841
|
+
* this module with `CREATE TABLE IF NOT EXISTS`. Only one of the two survives
|
|
51842
|
+
* arriving second — the migration dies on `relation already exists`, the control
|
|
51843
|
+
* plane restarts, and the self-host smoke reports it as a health-check timeout.
|
|
51844
|
+
*
|
|
51845
|
+
* The DDL here runs LAZILY, on the first request that builds the broker context,
|
|
51846
|
+
* so which of the two lands first is a coin flip decided by traffic. Probing the
|
|
51847
|
+
* database for a control plane cannot settle it: the box answers healthy seconds
|
|
51848
|
+
* before the control plane has written anything at all, so the probe would come
|
|
51849
|
+
* back empty and create the table anyway. So the topology DECLARES the owner
|
|
51850
|
+
* instead of the box guessing it, and a declared owner is the same answer on
|
|
51851
|
+
* every boot.
|
|
51852
|
+
*
|
|
51853
|
+
* `stackbone dev` and BYOC set nothing, have no control plane on their database,
|
|
51854
|
+
* and keep creating their own tables exactly as before.
|
|
51855
|
+
*/
|
|
51856
|
+
var ensureConnectTables = async (connectionString, opts = {}) => {
|
|
51857
|
+
if ((opts.schemaOwner ?? process.env["STACKBONE_PLATFORM_SCHEMA_OWNER"]) === "control-plane") return;
|
|
51654
51858
|
const client = new Client({ connectionString });
|
|
51655
51859
|
await client.connect();
|
|
51656
51860
|
try {
|
|
@@ -51720,47 +51924,53 @@ var deriveBase64Key = (raw) => createHash("sha256").update(raw).digest().toStrin
|
|
|
51720
51924
|
var makeContextFactory = (opts) => {
|
|
51721
51925
|
let cached;
|
|
51722
51926
|
const get = () => {
|
|
51723
|
-
if (!cached)
|
|
51724
|
-
|
|
51725
|
-
|
|
51726
|
-
|
|
51727
|
-
|
|
51728
|
-
|
|
51729
|
-
|
|
51730
|
-
|
|
51731
|
-
|
|
51732
|
-
|
|
51733
|
-
|
|
51734
|
-
|
|
51735
|
-
|
|
51736
|
-
|
|
51737
|
-
|
|
51738
|
-
|
|
51739
|
-
|
|
51740
|
-
|
|
51741
|
-
|
|
51742
|
-
|
|
51743
|
-
|
|
51744
|
-
|
|
51745
|
-
|
|
51927
|
+
if (!cached) {
|
|
51928
|
+
const building = (async () => {
|
|
51929
|
+
await ensureConnectTables(opts.connectionString);
|
|
51930
|
+
const cipher = await createSecretCipher(deriveBase64Key(opts.encryptionKey ?? DEV_CONNECT_ENCRYPTION_KEY));
|
|
51931
|
+
const sql = postgres(opts.connectionString, {
|
|
51932
|
+
max: 4,
|
|
51933
|
+
onnotice: () => void 0
|
|
51934
|
+
});
|
|
51935
|
+
const db = drizzle(sql);
|
|
51936
|
+
const store = new DrizzleBrokerStore(db, cipher);
|
|
51937
|
+
const cache = createInMemoryTokenCache();
|
|
51938
|
+
const specCache = createBoxSpecCache(opts.redisUrl);
|
|
51939
|
+
const bindings = createInMemoryBindingStore();
|
|
51940
|
+
const exchanger = new DefaultOAuthExchanger();
|
|
51941
|
+
const brokerCallbackBaseUrl = opts.brokerCallbackBaseUrl ?? "http://127.0.0.1:4242/api/connect/callback";
|
|
51942
|
+
const service = new ConnectService({
|
|
51943
|
+
store,
|
|
51944
|
+
exchanger,
|
|
51945
|
+
cache,
|
|
51946
|
+
bindings,
|
|
51947
|
+
oauth: {
|
|
51948
|
+
brokerCallbackUrl: brokerCallbackBaseUrl,
|
|
51949
|
+
stateSecret: deriveBase64Key(opts.encryptionKey ?? DEV_CONNECT_ENCRYPTION_KEY)
|
|
51950
|
+
}
|
|
51951
|
+
});
|
|
51952
|
+
return {
|
|
51953
|
+
db,
|
|
51954
|
+
audit: opts.audit,
|
|
51955
|
+
sql,
|
|
51956
|
+
cipher,
|
|
51957
|
+
store,
|
|
51958
|
+
service,
|
|
51959
|
+
bindings,
|
|
51960
|
+
exchanger,
|
|
51961
|
+
specCache: specCache.cache,
|
|
51962
|
+
brokerCallbackBaseUrl,
|
|
51963
|
+
async close() {
|
|
51964
|
+
await sql.end({ timeout: 5 }).catch(() => void 0);
|
|
51965
|
+
await specCache.close();
|
|
51966
|
+
}
|
|
51967
|
+
};
|
|
51968
|
+
})();
|
|
51969
|
+
cached = building;
|
|
51970
|
+
building.catch(() => {
|
|
51971
|
+
if (cached === building) cached = void 0;
|
|
51746
51972
|
});
|
|
51747
|
-
|
|
51748
|
-
db,
|
|
51749
|
-
audit: opts.audit,
|
|
51750
|
-
sql,
|
|
51751
|
-
cipher,
|
|
51752
|
-
store,
|
|
51753
|
-
service,
|
|
51754
|
-
bindings,
|
|
51755
|
-
exchanger,
|
|
51756
|
-
specCache: specCache.cache,
|
|
51757
|
-
brokerCallbackBaseUrl,
|
|
51758
|
-
async close() {
|
|
51759
|
-
await sql.end({ timeout: 5 }).catch(() => void 0);
|
|
51760
|
-
await specCache.close();
|
|
51761
|
-
}
|
|
51762
|
-
};
|
|
51763
|
-
})();
|
|
51973
|
+
}
|
|
51764
51974
|
return cached;
|
|
51765
51975
|
};
|
|
51766
51976
|
const close = async () => {
|
|
@@ -51785,11 +51995,12 @@ var init_broker_refusal = __esmMin((() => {
|
|
|
51785
51995
|
case "access_denied": return 401;
|
|
51786
51996
|
case "invalid_args": return 400;
|
|
51787
51997
|
case "invalid_grant": return 400;
|
|
51998
|
+
case "precondition_failed":
|
|
51999
|
+
case "cardinality_mismatch": return 409;
|
|
51788
52000
|
case "credential_error":
|
|
51789
52001
|
case "execute_failed":
|
|
51790
52002
|
case "timeout":
|
|
51791
52003
|
case "invalid_output": return 502;
|
|
51792
|
-
default: return 502;
|
|
51793
52004
|
}
|
|
51794
52005
|
};
|
|
51795
52006
|
brokerRefusal = (err) => {
|
|
@@ -53105,7 +53316,8 @@ var init_support = __esmMin((() => {
|
|
|
53105
53316
|
args: z.record(z.string(), z.unknown()).default({}),
|
|
53106
53317
|
principal: principalSchema.default({ type: "app" }),
|
|
53107
53318
|
installationId: z.string().min(1).optional(),
|
|
53108
|
-
runId: z.string().min(1).optional()
|
|
53319
|
+
runId: z.string().min(1).optional(),
|
|
53320
|
+
idempotent: z.boolean().optional()
|
|
53109
53321
|
});
|
|
53110
53322
|
requireBrokerSignature = (secret) => async (c, next) => {
|
|
53111
53323
|
if (!verifyTokenRequestSignature(c, secret)) return c.json({
|
|
@@ -53123,13 +53335,15 @@ var init_support = __esmMin((() => {
|
|
|
53123
53335
|
ok: false,
|
|
53124
53336
|
error: {
|
|
53125
53337
|
code: error.code,
|
|
53126
|
-
message: error.message
|
|
53338
|
+
message: error.message,
|
|
53339
|
+
...error.retryable !== void 0 ? { retryable: error.retryable } : {}
|
|
53127
53340
|
}
|
|
53128
53341
|
};
|
|
53129
53342
|
switch (statusForBrokerCode(error.code)) {
|
|
53130
53343
|
case 400: return c.json(body, 400);
|
|
53131
53344
|
case 401: return c.json(body, 401);
|
|
53132
53345
|
case 404: return c.json(body, 404);
|
|
53346
|
+
case 409: return c.json(body, 409);
|
|
53133
53347
|
default: return c.json(body, 502);
|
|
53134
53348
|
}
|
|
53135
53349
|
};
|
|
@@ -53269,13 +53483,14 @@ var createConnectTokenRouter = (opts, sharedContext) => {
|
|
|
53269
53483
|
security: HMAC_ROUTE_SECURITY,
|
|
53270
53484
|
middleware: [requireBrokerSignature(opts.hmacSecret)],
|
|
53271
53485
|
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
|
|
53486
|
+
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
53487
|
request: { body: jsonBody(named("ConnectExecuteRequest", executeRequestSchema), "The connector, the operation, and the args to run it with.") },
|
|
53274
53488
|
responses: {
|
|
53275
53489
|
200: jsonResponse(named("ConnectExecuteSuccess", connectExecuteSuccessSchema), "The operation’s own output, passed through untouched."),
|
|
53276
53490
|
400: executeRefusal("The body failed its schema (`invalid_payload`), or the broker refused the arguments (`invalid_args`, `invalid_grant`)."),
|
|
53277
53491
|
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
53492
|
404: executeRefusal("No such connector on this box, or no such operation on it (`not_found`, `connector_installation_required`)."),
|
|
53493
|
+
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
53494
|
502: executeRefusal("The provider call failed (`execute_failed`, `timeout`, `credential_error`, `invalid_output`). Upstream, not the box.")
|
|
53280
53495
|
}
|
|
53281
53496
|
}), async (c) => {
|
|
@@ -53291,7 +53506,8 @@ var createConnectTokenRouter = (opts, sharedContext) => {
|
|
|
53291
53506
|
connectConnectorId: request.connector,
|
|
53292
53507
|
connectOperationId: request.operation,
|
|
53293
53508
|
payload: request.args,
|
|
53294
|
-
...request.runId !== void 0 ? { runId: request.runId } : {}
|
|
53509
|
+
...request.runId !== void 0 ? { runId: request.runId } : {},
|
|
53510
|
+
...request.idempotent !== void 0 ? { idempotent: request.idempotent } : {}
|
|
53295
53511
|
});
|
|
53296
53512
|
if (result.ok) return c.json({
|
|
53297
53513
|
ok: true,
|
|
@@ -53299,7 +53515,8 @@ var createConnectTokenRouter = (opts, sharedContext) => {
|
|
|
53299
53515
|
}, 200);
|
|
53300
53516
|
return answerExecuteFailure(c, result.error);
|
|
53301
53517
|
} catch (err) {
|
|
53302
|
-
return
|
|
53518
|
+
if (isBrokerError(err)) return answerExecuteFailure(c, err);
|
|
53519
|
+
throw err;
|
|
53303
53520
|
}
|
|
53304
53521
|
});
|
|
53305
53522
|
app.openapi(createRoute({
|
|
@@ -65380,6 +65597,30 @@ var init_serial_execution_coordinator = __esmMin((() => {
|
|
|
65380
65597
|
createSerialExecutionCoordinator = (gate) => {
|
|
65381
65598
|
const held = /* @__PURE__ */ new Map();
|
|
65382
65599
|
return {
|
|
65600
|
+
async beginStart(workflowName) {
|
|
65601
|
+
const owner = held.get(workflowName);
|
|
65602
|
+
if (!owner) return null;
|
|
65603
|
+
const abandon = () => {
|
|
65604
|
+
if (held.get(workflowName) === owner) held.delete(workflowName);
|
|
65605
|
+
};
|
|
65606
|
+
try {
|
|
65607
|
+
if (!await gate.markAttempt(workflowName, owner.token)) {
|
|
65608
|
+
abandon();
|
|
65609
|
+
return null;
|
|
65610
|
+
}
|
|
65611
|
+
} catch (err) {
|
|
65612
|
+
abandon();
|
|
65613
|
+
throw err;
|
|
65614
|
+
}
|
|
65615
|
+
return {
|
|
65616
|
+
noteStarted(runId) {
|
|
65617
|
+
if (held.get(workflowName) === owner) owner.runId = runId;
|
|
65618
|
+
},
|
|
65619
|
+
acknowledge: () => gate.acknowledge(workflowName, owner.token),
|
|
65620
|
+
requeue: (trigger) => gate.requeue(workflowName, trigger, owner.token),
|
|
65621
|
+
abandon
|
|
65622
|
+
};
|
|
65623
|
+
},
|
|
65383
65624
|
async tryAdmit(workflowName, trigger) {
|
|
65384
65625
|
const verdict = await gate.tryAdmit(workflowName, trigger);
|
|
65385
65626
|
if (verdict.admitted) {
|
|
@@ -65420,26 +65661,34 @@ var init_serial_execution_coordinator = __esmMin((() => {
|
|
|
65420
65661
|
held.delete(workflowName);
|
|
65421
65662
|
},
|
|
65422
65663
|
async requeue(workflowName, trigger) {
|
|
65423
|
-
|
|
65664
|
+
const owner = held.get(workflowName);
|
|
65665
|
+
if (owner) await gate.requeue(workflowName, trigger, owner.token);
|
|
65424
65666
|
}
|
|
65425
65667
|
};
|
|
65426
65668
|
};
|
|
65427
65669
|
createSerialSweepSupport = (deps) => {
|
|
65428
65670
|
const startAdmitted = async (name, trigger) => {
|
|
65671
|
+
const attempt = await deps.coordinator.beginStart(name);
|
|
65672
|
+
if (!attempt) return;
|
|
65429
65673
|
let started;
|
|
65430
65674
|
try {
|
|
65431
65675
|
started = await deps.startNext(name, trigger);
|
|
65432
65676
|
} catch (err) {
|
|
65433
65677
|
try {
|
|
65434
|
-
await
|
|
65678
|
+
await attempt.requeue(trigger);
|
|
65435
65679
|
} catch (requeueErr) {
|
|
65436
65680
|
console.warn(`[serial-execution] could not re-enqueue an unstarted trigger for "${name}": ${requeueErr instanceof Error ? requeueErr.message : String(requeueErr)}`);
|
|
65437
65681
|
}
|
|
65438
|
-
|
|
65439
|
-
console.warn(`[serial-execution] failed to start admitted trigger for "${name}"
|
|
65682
|
+
attempt.abandon();
|
|
65683
|
+
console.warn(`[serial-execution] failed to start admitted trigger for "${name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
65440
65684
|
return;
|
|
65441
65685
|
}
|
|
65442
|
-
|
|
65686
|
+
attempt.noteStarted(started?.worldRunId);
|
|
65687
|
+
try {
|
|
65688
|
+
await attempt.acknowledge();
|
|
65689
|
+
} catch (err) {
|
|
65690
|
+
console.warn(`[serial-execution] could not acknowledge started trigger for "${name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
65691
|
+
}
|
|
65443
65692
|
await deps.clearWaiting?.(name, trigger.pendingId);
|
|
65444
65693
|
};
|
|
65445
65694
|
return {
|
|
@@ -65745,6 +65994,7 @@ var init_load_project_runtime = __esmMin((() => {
|
|
|
65745
65994
|
let setActiveStepReader;
|
|
65746
65995
|
let approvalRequest;
|
|
65747
65996
|
let setConnectorInvoker;
|
|
65997
|
+
let setEchoGuardReader;
|
|
65748
65998
|
let connectorInvoker;
|
|
65749
65999
|
let setActionInvoker;
|
|
65750
66000
|
let actionInvoker;
|
|
@@ -65760,6 +66010,7 @@ var init_load_project_runtime = __esmMin((() => {
|
|
|
65760
66010
|
setApprovalRecorder = sdk.setApprovalRecorder;
|
|
65761
66011
|
setActiveStepReader = sdk.setActiveStepReader;
|
|
65762
66012
|
setConnectorInvoker = sdk.setConnectorInvoker;
|
|
66013
|
+
setEchoGuardReader = sdk.setEchoGuardReader;
|
|
65763
66014
|
connectorInvoker = sdk.createConnectInvoker?.();
|
|
65764
66015
|
setActionInvoker = sdk.setActionInvoker;
|
|
65765
66016
|
actionInvoker = sdk.createActionInvoker?.();
|
|
@@ -65817,6 +66068,7 @@ var init_load_project_runtime = __esmMin((() => {
|
|
|
65817
66068
|
setActiveStepReader,
|
|
65818
66069
|
approvalRequest,
|
|
65819
66070
|
setConnectorInvoker,
|
|
66071
|
+
setEchoGuardReader,
|
|
65820
66072
|
connectorInvoker,
|
|
65821
66073
|
setActionInvoker,
|
|
65822
66074
|
actionInvoker,
|
|
@@ -66481,11 +66733,49 @@ var init_runtime_approval_plants = __esmMin((() => {
|
|
|
66481
66733
|
}));
|
|
66482
66734
|
//#endregion
|
|
66483
66735
|
//#region ../../libs/runtime/workspace-runtime/src/lib/workflow/runtime-connector-plants.ts
|
|
66484
|
-
var plantConnectorInvoker, plantActionInvoker;
|
|
66736
|
+
var owningWorkflow, echoOwner, unresolvedEcho, resolveEcho, echoRecordError, plantConnectorInvoker, plantActionInvoker, ECHO_WINDOW_MS, renderEcho, echoGuardUnavailable, plantEchoGuard;
|
|
66485
66737
|
var init_runtime_connector_plants = __esmMin((() => {
|
|
66738
|
+
init_src$4();
|
|
66739
|
+
init_src$13();
|
|
66486
66740
|
init_call_ordinal();
|
|
66487
66741
|
init_monotonic_span();
|
|
66488
66742
|
init_project_runtime();
|
|
66743
|
+
owningWorkflow = () => {
|
|
66744
|
+
const owner = getExecutionOwner();
|
|
66745
|
+
return owner?.kind === "workflow" ? owner.name : void 0;
|
|
66746
|
+
};
|
|
66747
|
+
echoOwner = () => {
|
|
66748
|
+
const workflowName = owningWorkflow();
|
|
66749
|
+
return workflowName !== void 0 ? { workflowName } : {};
|
|
66750
|
+
};
|
|
66751
|
+
unresolvedEcho = (echoPath) => ({
|
|
66752
|
+
echoPath,
|
|
66753
|
+
echo: null,
|
|
66754
|
+
echoUnresolved: true,
|
|
66755
|
+
...echoOwner()
|
|
66756
|
+
});
|
|
66757
|
+
resolveEcho = (echoPath, output, label) => {
|
|
66758
|
+
const owner = echoOwner();
|
|
66759
|
+
const echo = getPath(output, echoPath);
|
|
66760
|
+
if (echo !== void 0 && echo !== null && echo !== "") return {
|
|
66761
|
+
echoPath,
|
|
66762
|
+
echo,
|
|
66763
|
+
...owner
|
|
66764
|
+
};
|
|
66765
|
+
console.warn(`[connector] ${label}: echoPath "${echoPath}" resolved to nothing (missing, null or empty) in the response, so the run step records no echo value and the self-write guard cannot answer for this call`);
|
|
66766
|
+
return {
|
|
66767
|
+
echoPath,
|
|
66768
|
+
echo: null,
|
|
66769
|
+
echoUnresolved: true,
|
|
66770
|
+
...owner
|
|
66771
|
+
};
|
|
66772
|
+
};
|
|
66773
|
+
echoRecordError = (label, cause) => {
|
|
66774
|
+
const error = /* @__PURE__ */ new Error(`[connector] ${label}: the provider call succeeded but its echo record could not be written (${cause instanceof Error ? cause.message : String(cause)}), so nothing can recognise this write when the trigger delivers it back. The step stops here.`);
|
|
66775
|
+
error.code = "echo_record_failed";
|
|
66776
|
+
error.fatal = true;
|
|
66777
|
+
return error;
|
|
66778
|
+
};
|
|
66489
66779
|
plantConnectorInvoker = (rt, ctx) => {
|
|
66490
66780
|
if (!rt.setConnectorInvoker || !rt.connectorInvoker) return;
|
|
66491
66781
|
const baseInvoker = rt.connectorInvoker;
|
|
@@ -66499,14 +66789,15 @@ var init_runtime_connector_plants = __esmMin((() => {
|
|
|
66499
66789
|
const span = startSpan();
|
|
66500
66790
|
const ordinal = stepInfo ? nextConnOrdinal(runId ?? "", stepInfo.stepId, stepInfo.attempt) : 0;
|
|
66501
66791
|
const externalStepId = stepInfo ? `${stepInfo.stepId}::conn::${ordinal}` : randomUUID();
|
|
66502
|
-
const
|
|
66503
|
-
|
|
66792
|
+
const label = `${args.connector}/${args.operation}`;
|
|
66793
|
+
const writeRow = (status, extra, echo) => {
|
|
66794
|
+
if (!runId) return void 0;
|
|
66504
66795
|
const { completedAt, durationMs } = closeSpan(span);
|
|
66505
|
-
recordActivityStep({
|
|
66796
|
+
return recordActivityStep({
|
|
66506
66797
|
runId,
|
|
66507
66798
|
externalStepId,
|
|
66508
66799
|
type: "connection",
|
|
66509
|
-
name:
|
|
66800
|
+
name: label,
|
|
66510
66801
|
status,
|
|
66511
66802
|
startedAt: span.startedAt.toISOString(),
|
|
66512
66803
|
completedAt: completedAt.toISOString(),
|
|
@@ -66514,26 +66805,52 @@ var init_runtime_connector_plants = __esmMin((() => {
|
|
|
66514
66805
|
input: {
|
|
66515
66806
|
connector: args.connector,
|
|
66516
66807
|
operation: args.operation,
|
|
66517
|
-
args: args.args
|
|
66808
|
+
args: args.args,
|
|
66809
|
+
...echo ?? {}
|
|
66518
66810
|
},
|
|
66519
66811
|
...extra,
|
|
66520
66812
|
enclosingExternalStepId: stepInfo?.stepId ?? null
|
|
66521
|
-
})
|
|
66813
|
+
});
|
|
66814
|
+
};
|
|
66815
|
+
const forget = (write) => {
|
|
66816
|
+
write?.catch((err) => {
|
|
66522
66817
|
console.warn(`[connector] run-step write failed for run ${runId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
66523
66818
|
});
|
|
66524
66819
|
};
|
|
66820
|
+
let result;
|
|
66525
66821
|
try {
|
|
66526
|
-
|
|
66527
|
-
record("completed", { output: result.output });
|
|
66528
|
-
return result;
|
|
66822
|
+
result = await baseInvoker.invoke(args);
|
|
66529
66823
|
} catch (err) {
|
|
66530
66824
|
const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
66531
|
-
|
|
66825
|
+
const echo = typeof args.echoPath === "string" ? unresolvedEcho(args.echoPath) : void 0;
|
|
66826
|
+
if (echo) console.warn(`[connector] ${label}: the call failed after naming an echoPath, so it may have landed with nothing recorded — the run step keeps an unresolved marker and the self-write guard will refuse rather than answer for this connector`);
|
|
66827
|
+
const write = writeRow("failed", { error: {
|
|
66532
66828
|
message: err instanceof Error ? err.message : String(err),
|
|
66533
66829
|
...typeof code === "string" ? { code } : {}
|
|
66534
|
-
} });
|
|
66830
|
+
} }, echo);
|
|
66831
|
+
if (echo) await write?.catch((cause) => {
|
|
66832
|
+
console.warn(`[connector] ${label}: could not record the unresolved echo marker: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
66833
|
+
});
|
|
66834
|
+
else forget(write);
|
|
66535
66835
|
throw err;
|
|
66536
66836
|
}
|
|
66837
|
+
if (typeof args.echoPath !== "string") {
|
|
66838
|
+
forget(writeRow("completed", { output: result.output }));
|
|
66839
|
+
return result;
|
|
66840
|
+
}
|
|
66841
|
+
const echo = resolveEcho(args.echoPath, result.output, label);
|
|
66842
|
+
const write = writeRow("completed", { output: result.output }, echo);
|
|
66843
|
+
if (write === void 0) console.warn(`[connector] ${label}: this call named an echoPath but runs with no invocation context, so no run step records its echo and the self-write guard cannot recognise the delivery this write causes`);
|
|
66844
|
+
if (write !== void 0) {
|
|
66845
|
+
let rowId;
|
|
66846
|
+
try {
|
|
66847
|
+
rowId = await write;
|
|
66848
|
+
} catch (cause) {
|
|
66849
|
+
throw echoRecordError(label, cause);
|
|
66850
|
+
}
|
|
66851
|
+
if (rowId === null) throw echoRecordError(label, /* @__PURE__ */ new Error(`the store wrote no row for run ${runId}`));
|
|
66852
|
+
}
|
|
66853
|
+
return result;
|
|
66537
66854
|
} });
|
|
66538
66855
|
} else rt.setConnectorInvoker(baseInvoker);
|
|
66539
66856
|
};
|
|
@@ -66597,6 +66914,34 @@ var init_runtime_connector_plants = __esmMin((() => {
|
|
|
66597
66914
|
}
|
|
66598
66915
|
} });
|
|
66599
66916
|
};
|
|
66917
|
+
ECHO_WINDOW_MS = 3600 * 1e3;
|
|
66918
|
+
renderEcho = (value) => String(value);
|
|
66919
|
+
echoGuardUnavailable = (connector, workflowName) => {
|
|
66920
|
+
const scope = workflowName ? `this workflow ("${workflowName}"), or a caller with no workflow owner,` : "some caller on this connector";
|
|
66921
|
+
const error = /* @__PURE__ */ new Error(`isOwnEcho("${connector}"): a write by ${scope} inside the last hour named an echoPath that resolved to nothing, so the value it produced was never recorded. The guard cannot tell your own write from somebody else's change. Find the echoPath that resolved to nothing — the runtime warned which one when it wrote the row.`);
|
|
66922
|
+
error.code = "echo_guard_unavailable";
|
|
66923
|
+
return error;
|
|
66924
|
+
};
|
|
66925
|
+
plantEchoGuard = (rt, ctx) => {
|
|
66926
|
+
if (!rt.setEchoGuardReader) return;
|
|
66927
|
+
const findEchoRecord = ctx.invocations?.findEchoRecord?.bind(ctx.invocations);
|
|
66928
|
+
if (!findEchoRecord) {
|
|
66929
|
+
rt.setEchoGuardReader(void 0);
|
|
66930
|
+
return;
|
|
66931
|
+
}
|
|
66932
|
+
rt.setEchoGuardReader({ isOwnEcho: async ({ connector, value }) => {
|
|
66933
|
+
const workflowName = owningWorkflow();
|
|
66934
|
+
const { matched, unresolved } = await findEchoRecord({
|
|
66935
|
+
connector,
|
|
66936
|
+
value: renderEcho(value),
|
|
66937
|
+
workflowName: workflowName ?? null,
|
|
66938
|
+
windowMs: ECHO_WINDOW_MS
|
|
66939
|
+
});
|
|
66940
|
+
if (matched) return true;
|
|
66941
|
+
if (unresolved) throw echoGuardUnavailable(connector, workflowName);
|
|
66942
|
+
return false;
|
|
66943
|
+
} });
|
|
66944
|
+
};
|
|
66600
66945
|
}));
|
|
66601
66946
|
//#endregion
|
|
66602
66947
|
//#region ../../libs/runtime/workspace-runtime/src/lib/workflow/runtime-sdk-plants.ts
|
|
@@ -66660,6 +67005,7 @@ var init_runtime_sdk_plants = __esmMin((() => {
|
|
|
66660
67005
|
plantPromptUseReporter(rt, ctx);
|
|
66661
67006
|
plantApprovalRecorder(rt, ctx);
|
|
66662
67007
|
plantConnectorInvoker(rt, ctx);
|
|
67008
|
+
plantEchoGuard(rt, ctx);
|
|
66663
67009
|
plantActionInvoker(rt, ctx);
|
|
66664
67010
|
return rt;
|
|
66665
67011
|
};
|
|
@@ -66686,6 +67032,7 @@ var init_runtime_loader = __esmMin((() => {
|
|
|
66686
67032
|
rt.setPromptUseReporter?.(void 0);
|
|
66687
67033
|
rt.setApprovalRecorder?.(void 0);
|
|
66688
67034
|
rt.setConnectorInvoker?.(void 0);
|
|
67035
|
+
rt.setEchoGuardReader?.(void 0);
|
|
66689
67036
|
rt.setActionInvoker?.(void 0);
|
|
66690
67037
|
rt.setActiveStepReader?.(void 0);
|
|
66691
67038
|
planted = void 0;
|
|
@@ -67254,9 +67601,12 @@ var init_event_forwarder = __esmMin((() => {
|
|
|
67254
67601
|
* the row `claimed` while the attempt went ahead; a later swallowed `markStarted`
|
|
67255
67602
|
* would then leave a `claimed` row with a real run behind it, the sweep would
|
|
67256
67603
|
* release it, and the customer's workflow would run a second time. Failing here
|
|
67257
|
-
*
|
|
67258
|
-
*
|
|
67259
|
-
* the
|
|
67604
|
+
* instead bubbles without releasing: a failed marker cannot establish ownership,
|
|
67605
|
+
* and deleting by pair could erase another writer's delivery. The tick holds its
|
|
67606
|
+
* cursor. If the row remains `claimed`, recovery waits for the takeover window
|
|
67607
|
+
* and consumes takeover budget, even for a transient marker failure. An applied
|
|
67608
|
+
* update whose response is lost instead leaves `starting` without an attempt;
|
|
67609
|
+
* this slice cannot resolve that unknown outcome (PLA-1395 investigation).
|
|
67260
67610
|
*
|
|
67261
67611
|
* Written at most once: a resume that misses falls through to the start below.
|
|
67262
67612
|
*/
|
|
@@ -67265,13 +67615,7 @@ var init_event_forwarder = __esmMin((() => {
|
|
|
67265
67615
|
* one must give up where it stands. */
|
|
67266
67616
|
const markAttempt = async () => {
|
|
67267
67617
|
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
|
-
}
|
|
67618
|
+
const matched = await deps.deliveries.markStarting(subscription.id, event.id);
|
|
67275
67619
|
attemptMarked = matched;
|
|
67276
67620
|
return matched;
|
|
67277
67621
|
};
|
|
@@ -70958,7 +71302,7 @@ var calls_workflow_ts_default = "import { z } from \"zod\";\nimport { streamDeep
|
|
|
70958
71302
|
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
71303
|
//#endregion
|
|
70960
71304
|
//#region src/services/scaffold/template-loader.ts
|
|
70961
|
-
var SDK_TEMPLATE_VERSION = "0.
|
|
71305
|
+
var SDK_TEMPLATE_VERSION = "0.5.0";
|
|
70962
71306
|
var PREFIX = "./templates/";
|
|
70963
71307
|
var templates = new Map(Object.entries(/* @__PURE__ */ Object.assign({
|
|
70964
71308
|
"./templates/deep-agents/index.ts.tmpl": index_ts_default,
|
|
@@ -75983,6 +76327,15 @@ ${objectStore}
|
|
|
75983
76327
|
# MinIO addresses buckets by path. A managed bucket that only answers to the
|
|
75984
76328
|
# bucket-as-subdomain form (AWS S3, Railway Buckets) needs this set to false.
|
|
75985
76329
|
STACKBONE_S3_FORCE_PATH_STYLE: \${STACKBONE_S3_FORCE_PATH_STYLE:-true}
|
|
76330
|
+
# PLA-1286 — WHO OWNS THE PLATFORM SCHEMA IN THE DATABASE BELOW. This is the
|
|
76331
|
+
# only topology where the agent and the control plane share one Postgres,
|
|
76332
|
+
# and both of them declare \`connection_grant\`: the control plane through
|
|
76333
|
+
# its db-core migrations, the agent through its own boot DDL. The control
|
|
76334
|
+
# plane's statement is a bare CREATE TABLE and dies if the table is already
|
|
76335
|
+
# there, so the agent stands down here and lets the migrations create them.
|
|
76336
|
+
# Not overridable from .env on purpose: it is a fact about this compose file,
|
|
76337
|
+
# not a preference, and a box that shares no database never sees this line.
|
|
76338
|
+
STACKBONE_PLATFORM_SCHEMA_OWNER: control-plane
|
|
75986
76339
|
${sharedIdentity}
|
|
75987
76340
|
# REPLACES the agent's built-in origin allowlist — it does not add to it. The
|
|
75988
76341
|
# dashboard is a different ORIGIN from the agent (different port), so its
|
|
@@ -78802,6 +79155,7 @@ var generateConnectorTypes = (models) => {
|
|
|
78802
79155
|
HEADER,
|
|
78803
79156
|
"",
|
|
78804
79157
|
...AUGMENTED_MODULES.map((mod) => `import ${JSON.stringify(mod)};`),
|
|
79158
|
+
`import type { CallConnectorOptions } from ${JSON.stringify(AUGMENTED_MODULES[0])};`,
|
|
78805
79159
|
"",
|
|
78806
79160
|
augmentations.join("\n"),
|
|
78807
79161
|
""
|
|
@@ -78817,15 +79171,32 @@ function renderConnectorEntry(model) {
|
|
|
78817
79171
|
].join("\n");
|
|
78818
79172
|
}
|
|
78819
79173
|
/**
|
|
78820
|
-
* One ` <op>(args: <type
|
|
78821
|
-
* THREE overload lines a `'when-requested'` raw response
|
|
78822
|
-
* signature there has two reachable outcomes.
|
|
79174
|
+
* One ` <op>(args: <type>, opts?: CallConnectorOptions): Promise<<out>>;` line
|
|
79175
|
+
* (indent level 3) — or the THREE overload lines a `'when-requested'` raw response
|
|
79176
|
+
* takes, because one signature there has two reachable outcomes.
|
|
78823
79177
|
*/
|
|
78824
79178
|
function renderOperation(op) {
|
|
78825
79179
|
if (takesEitherOutcome(op)) return renderEitherOutcome(op);
|
|
78826
|
-
return ` ${propertyKey(op.name)}(${renderParam(op)}): Promise<${renderReturn(op)}>;`;
|
|
79180
|
+
return ` ${propertyKey(op.name)}(${renderParam(op)}${OPTS_PARAM}): Promise<${renderReturn(op)}>;`;
|
|
78827
79181
|
}
|
|
78828
79182
|
/**
|
|
79183
|
+
* The second parameter every generated signature carries (PLA-1349).
|
|
79184
|
+
*
|
|
79185
|
+
* `connection(id).<op>(args, opts)` has always forwarded a second argument at run
|
|
79186
|
+
* time (`call-connector.ts`), and no generated signature declared it — so
|
|
79187
|
+
* `idempotent` was reachable only through the untyped `.call(...)`, which is not
|
|
79188
|
+
* the path a creator is steered to. Typed as the whole options object rather than
|
|
79189
|
+
* one field, which is what carried PLA-1351's `echoPath` in with no change here:
|
|
79190
|
+
* the option landed on `CallConnectorOptions` and every generated signature already
|
|
79191
|
+
* accepted it.
|
|
79192
|
+
*
|
|
79193
|
+
* It is ALWAYS second, and `renderParam` is what guarantees that: an operation
|
|
79194
|
+
* with nothing to put in `args` still prints `args?: undefined` ahead of this. The
|
|
79195
|
+
* proxy is `(args?, opts?)` for every operation, so a signature that let `opts`
|
|
79196
|
+
* come first would hand the caller's options to the provider as arguments.
|
|
79197
|
+
*/
|
|
79198
|
+
var OPTS_PARAM = ", opts?: CallConnectorOptions";
|
|
79199
|
+
/**
|
|
78829
79200
|
* Does ONE signature of this operation have two reachable outcomes?
|
|
78830
79201
|
*
|
|
78831
79202
|
* Only `'when-requested'`, and only while the switch is actually IN its arguments
|
|
@@ -78865,7 +79236,7 @@ function takesEitherOutcome(op) {
|
|
|
78865
79236
|
function renderEitherOutcome(op) {
|
|
78866
79237
|
const receipt = printType(rawResponseReceiptJsonSchema(), 3);
|
|
78867
79238
|
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}>;`;
|
|
79239
|
+
const signature = (presence, out) => ` ${propertyKey(op.name)}(args: ${printType(withKeyPresence(op.argsSchema, presence), 3)}${OPTS_PARAM}): Promise<${out}>;`;
|
|
78869
79240
|
return [
|
|
78870
79241
|
signature("required", receipt),
|
|
78871
79242
|
signature("forbidden", response),
|
|
@@ -78881,6 +79252,8 @@ function renderEitherOutcome(op) {
|
|
|
78881
79252
|
*/
|
|
78882
79253
|
function withKeyPresence(schema, presence) {
|
|
78883
79254
|
if (!isObject(schema)) return schema;
|
|
79255
|
+
const union = schema["anyOf"] ?? schema["oneOf"];
|
|
79256
|
+
if (Array.isArray(union)) return { anyOf: union.map((branch) => withKeyPresence(branch, presence)) };
|
|
78884
79257
|
const properties = { ...isObject(schema["properties"]) ? schema["properties"] : {} };
|
|
78885
79258
|
const required = new Set(Array.isArray(schema["required"]) ? schema["required"].filter((k) => typeof k === "string") : []);
|
|
78886
79259
|
if (presence === "required") required.add(RAW_RESPONSE_KEY_ARG);
|
|
@@ -78896,7 +79269,7 @@ function withKeyPresence(schema, presence) {
|
|
|
78896
79269
|
}
|
|
78897
79270
|
/** `args: <type>` for an operation with arguments, and nothing for one without. */
|
|
78898
79271
|
function renderParam(op) {
|
|
78899
|
-
return schemaHasProperties(op.argsSchema) ? `args: ${printType(op.argsSchema, 3)}` : "";
|
|
79272
|
+
return schemaHasProperties(op.argsSchema) ? `args: ${printType(op.argsSchema, 3)}` : "args?: undefined";
|
|
78900
79273
|
}
|
|
78901
79274
|
/**
|
|
78902
79275
|
* What the operation ACTUALLY answers (PLA-1203).
|
|
@@ -79142,8 +79515,9 @@ function answeredResponseSchema(op, byId) {
|
|
|
79142
79515
|
* BROKER-consumed arguments into ONE flat args object schema — exactly how the
|
|
79143
79516
|
* broker executor reads the payload (`args[param.name]` for path/query, the
|
|
79144
79517
|
* 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
|
-
*
|
|
79518
|
+
* request body (rare) is dropped from the flat shape. Swagger's named body is
|
|
79519
|
+
* also offered as an optional argument for writes whose column names overlap
|
|
79520
|
+
* query filters.
|
|
79147
79521
|
*
|
|
79148
79522
|
* The three sources are merged in the order the executor reads them, and the
|
|
79149
79523
|
* broker's come LAST because they are the only ones it will not put on the wire.
|
|
@@ -79167,19 +79541,44 @@ function operationArgsSchema(op, trustBodyRequired) {
|
|
|
79167
79541
|
Object.assign(properties, bodyProperties);
|
|
79168
79542
|
if (trustBodyRequired && Array.isArray(body["required"])) required.push(...body["required"].filter((k) => typeof k === "string" && !isServerGenerated(bodyProperties[k])));
|
|
79169
79543
|
}
|
|
79544
|
+
const bodyRequired = [...required];
|
|
79545
|
+
const sharedRequired = [];
|
|
79546
|
+
const consumedNames = /* @__PURE__ */ new Set();
|
|
79547
|
+
if (op.bodyArgName !== void 0 && isObject(body)) {
|
|
79548
|
+
const namedBody = { ...body };
|
|
79549
|
+
if (!trustBodyRequired) delete namedBody["required"];
|
|
79550
|
+
else if (Array.isArray(body["required"]) && isObject(body["properties"])) {
|
|
79551
|
+
const bodyProperties = body["properties"];
|
|
79552
|
+
namedBody["required"] = body["required"].filter((key) => typeof key === "string" && !isServerGenerated(bodyProperties[key]));
|
|
79553
|
+
}
|
|
79554
|
+
properties[op.bodyArgName] = namedBody;
|
|
79555
|
+
}
|
|
79170
79556
|
for (const param of op.parameters ?? []) {
|
|
79171
79557
|
properties[param.name] = param.schema ?? {};
|
|
79172
|
-
|
|
79558
|
+
consumedNames.add(param.name);
|
|
79559
|
+
if (param.required) {
|
|
79560
|
+
required.push(param.name);
|
|
79561
|
+
sharedRequired.push(param.name);
|
|
79562
|
+
}
|
|
79173
79563
|
}
|
|
79174
79564
|
for (const arg of brokerConsumedArgs(op)) {
|
|
79175
79565
|
properties[arg.name] = arg.schema;
|
|
79176
|
-
|
|
79566
|
+
consumedNames.add(arg.name);
|
|
79567
|
+
if (arg.required) {
|
|
79568
|
+
required.push(arg.name);
|
|
79569
|
+
sharedRequired.push(arg.name);
|
|
79570
|
+
}
|
|
79177
79571
|
}
|
|
79178
|
-
|
|
79572
|
+
const flat = {
|
|
79179
79573
|
type: "object",
|
|
79180
79574
|
properties,
|
|
79181
79575
|
...required.length > 0 ? { required: [...new Set(required)] } : {}
|
|
79182
79576
|
};
|
|
79577
|
+
if (op.bodyArgName === void 0 || bodyRequired.length === 0 || consumedNames.has(op.bodyArgName)) return flat;
|
|
79578
|
+
return { anyOf: [flat, {
|
|
79579
|
+
...flat,
|
|
79580
|
+
required: [...new Set([...sharedRequired, op.bodyArgName])]
|
|
79581
|
+
}] };
|
|
79183
79582
|
}
|
|
79184
79583
|
/**
|
|
79185
79584
|
* Whether a body property is one the SERVER produces rather than one the caller
|
|
@@ -79202,11 +79601,17 @@ function isServerGenerated(property) {
|
|
|
79202
79601
|
}
|
|
79203
79602
|
/** Whether an object schema declares ONE named property. */
|
|
79204
79603
|
function schemaHasProperty(schema, name) {
|
|
79205
|
-
|
|
79604
|
+
if (!isObject(schema)) return false;
|
|
79605
|
+
const union = schema["anyOf"] ?? schema["oneOf"];
|
|
79606
|
+
if (Array.isArray(union)) return union.some((branch) => schemaHasProperty(branch, name));
|
|
79607
|
+
return isObject(schema["properties"]) && name in schema["properties"];
|
|
79206
79608
|
}
|
|
79207
79609
|
/** Whether an object schema declares at least one property (else the op takes no args). */
|
|
79208
79610
|
function schemaHasProperties(schema) {
|
|
79209
|
-
|
|
79611
|
+
if (!isObject(schema)) return false;
|
|
79612
|
+
const union = schema["anyOf"] ?? schema["oneOf"];
|
|
79613
|
+
if (Array.isArray(union)) return union.some(schemaHasProperties);
|
|
79614
|
+
return isObject(schema["properties"]) && Object.keys(schema["properties"]).length > 0;
|
|
79210
79615
|
}
|
|
79211
79616
|
/**
|
|
79212
79617
|
* Print a TypeScript type literal for a JSON Schema node. Handles the subset
|
|
@@ -81823,6 +82228,64 @@ var createDevCommand = (ctx) => defineCommand({
|
|
|
81823
82228
|
})(args)
|
|
81824
82229
|
});
|
|
81825
82230
|
//#endregion
|
|
82231
|
+
//#region ../../libs/shared/format/src/lib/coding-agent-prompt.ts
|
|
82232
|
+
/**
|
|
82233
|
+
* The pasteable prompt, and nothing around it, with no "open your coding agent"
|
|
82234
|
+
* preamble. Each surface draws its own instruction: the CLI as a `note`, the
|
|
82235
|
+
* web as the label above a copy button.
|
|
82236
|
+
*
|
|
82237
|
+
* `null` is the empty workspace, which is what the web always passes: the
|
|
82238
|
+
* browser cannot see which piece the reader picked at `init`'s own prompt. That
|
|
82239
|
+
* costs nothing, because `stackbone-coder` detects the workspace on disk by
|
|
82240
|
+
* itself and adds to it rather than re-scaffolding.
|
|
82241
|
+
*/
|
|
82242
|
+
function codingAgentPrompt(piece) {
|
|
82243
|
+
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.";
|
|
82244
|
+
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.`;
|
|
82245
|
+
}
|
|
82246
|
+
//#endregion
|
|
82247
|
+
//#region ../../libs/shared/format/src/lib/coding-agents.ts
|
|
82248
|
+
var CODING_AGENT_LABELS = [
|
|
82249
|
+
{
|
|
82250
|
+
id: "claude-code",
|
|
82251
|
+
label: "Claude Code"
|
|
82252
|
+
},
|
|
82253
|
+
{
|
|
82254
|
+
id: "cursor",
|
|
82255
|
+
label: "Cursor"
|
|
82256
|
+
},
|
|
82257
|
+
{
|
|
82258
|
+
id: "github-copilot",
|
|
82259
|
+
label: "GitHub Copilot (VS Code)"
|
|
82260
|
+
},
|
|
82261
|
+
{
|
|
82262
|
+
id: "gemini-cli",
|
|
82263
|
+
label: "Gemini CLI"
|
|
82264
|
+
},
|
|
82265
|
+
{
|
|
82266
|
+
id: "windsurf",
|
|
82267
|
+
label: "Windsurf"
|
|
82268
|
+
},
|
|
82269
|
+
{
|
|
82270
|
+
id: "cline",
|
|
82271
|
+
label: "Cline"
|
|
82272
|
+
},
|
|
82273
|
+
{
|
|
82274
|
+
id: "codex",
|
|
82275
|
+
label: "Codex CLI"
|
|
82276
|
+
}
|
|
82277
|
+
];
|
|
82278
|
+
/**
|
|
82279
|
+
* Throws rather than falling back to the id: a registry row whose label is
|
|
82280
|
+
* missing here is a mistake to fix at build time, not a menu entry reading
|
|
82281
|
+
* `github-copilot`.
|
|
82282
|
+
*/
|
|
82283
|
+
var codingAgentLabel = (id) => {
|
|
82284
|
+
const found = CODING_AGENT_LABELS.find((agent) => agent.id === id);
|
|
82285
|
+
if (!found) throw new Error(`no label for coding agent "${id}"`);
|
|
82286
|
+
return found.label;
|
|
82287
|
+
};
|
|
82288
|
+
//#endregion
|
|
81826
82289
|
//#region src/services/coding-agents/registry.ts
|
|
81827
82290
|
/**
|
|
81828
82291
|
* The one place that knows which coding agents Stackbone supports and what
|
|
@@ -81831,6 +82294,10 @@ var createDevCommand = (ctx) => defineCommand({
|
|
|
81831
82294
|
* that pre-ticks the menu, and the MCP config writer — so adding an agent is a
|
|
81832
82295
|
* row here and nothing else.
|
|
81833
82296
|
*
|
|
82297
|
+
* The one thing a row does NOT own is its label: that is in `@stackbone/format`,
|
|
82298
|
+
* because the browser renders the same list and cannot import this file — it
|
|
82299
|
+
* resolves paths against `homedir()` and reads `process.platform`.
|
|
82300
|
+
*
|
|
81834
82301
|
* The four DIFFERENT MCP entry shapes below are the reason this is data rather
|
|
81835
82302
|
* than a switch. There is no convention: `mcpServers` vs `servers`, `url` vs
|
|
81836
82303
|
* `httpUrl` vs `serverUrl`, `http` vs `streamableHttp`, JSON vs TOML. Each was
|
|
@@ -81870,7 +82337,7 @@ var CODEX_MCP = {
|
|
|
81870
82337
|
var CODING_AGENTS = [
|
|
81871
82338
|
{
|
|
81872
82339
|
id: "claude-code",
|
|
81873
|
-
label: "
|
|
82340
|
+
label: codingAgentLabel("claude-code"),
|
|
81874
82341
|
skillsId: "claude-code",
|
|
81875
82342
|
skillsDir: ".claude",
|
|
81876
82343
|
detect: [".claude", "~/.claude"],
|
|
@@ -81884,7 +82351,7 @@ var CODING_AGENTS = [
|
|
|
81884
82351
|
},
|
|
81885
82352
|
{
|
|
81886
82353
|
id: "cursor",
|
|
81887
|
-
label: "
|
|
82354
|
+
label: codingAgentLabel("cursor"),
|
|
81888
82355
|
skillsId: "cursor",
|
|
81889
82356
|
skillsDir: ".agents",
|
|
81890
82357
|
detect: [".cursor", "~/.cursor"],
|
|
@@ -81898,7 +82365,7 @@ var CODING_AGENTS = [
|
|
|
81898
82365
|
},
|
|
81899
82366
|
{
|
|
81900
82367
|
id: "github-copilot",
|
|
81901
|
-
label: "
|
|
82368
|
+
label: codingAgentLabel("github-copilot"),
|
|
81902
82369
|
skillsId: "github-copilot",
|
|
81903
82370
|
skillsDir: ".agents",
|
|
81904
82371
|
detect: [".vscode"],
|
|
@@ -81912,7 +82379,7 @@ var CODING_AGENTS = [
|
|
|
81912
82379
|
},
|
|
81913
82380
|
{
|
|
81914
82381
|
id: "gemini-cli",
|
|
81915
|
-
label: "
|
|
82382
|
+
label: codingAgentLabel("gemini-cli"),
|
|
81916
82383
|
skillsId: "gemini-cli",
|
|
81917
82384
|
skillsDir: ".agents",
|
|
81918
82385
|
detect: [".gemini", "~/.gemini"],
|
|
@@ -81926,7 +82393,7 @@ var CODING_AGENTS = [
|
|
|
81926
82393
|
},
|
|
81927
82394
|
{
|
|
81928
82395
|
id: "windsurf",
|
|
81929
|
-
label: "
|
|
82396
|
+
label: codingAgentLabel("windsurf"),
|
|
81930
82397
|
skillsId: "windsurf",
|
|
81931
82398
|
skillsDir: ".windsurf",
|
|
81932
82399
|
detect: [".windsurf", "~/.codeium"],
|
|
@@ -81944,7 +82411,7 @@ var CODING_AGENTS = [
|
|
|
81944
82411
|
},
|
|
81945
82412
|
{
|
|
81946
82413
|
id: "cline",
|
|
81947
|
-
label: "
|
|
82414
|
+
label: codingAgentLabel("cline"),
|
|
81948
82415
|
skillsId: "cline",
|
|
81949
82416
|
skillsDir: ".agents",
|
|
81950
82417
|
detect: [
|
|
@@ -81965,7 +82432,7 @@ var CODING_AGENTS = [
|
|
|
81965
82432
|
},
|
|
81966
82433
|
{
|
|
81967
82434
|
id: "codex",
|
|
81968
|
-
label: "
|
|
82435
|
+
label: codingAgentLabel("codex"),
|
|
81969
82436
|
skillsId: "codex",
|
|
81970
82437
|
skillsDir: ".agents",
|
|
81971
82438
|
detect: [".codex", "~/.codex"],
|
|
@@ -82642,22 +83109,6 @@ var createHitlCommand = defineGroup({
|
|
|
82642
83109
|
}
|
|
82643
83110
|
});
|
|
82644
83111
|
//#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
83112
|
//#region src/core/npm-exec.ts
|
|
82662
83113
|
var execFileAsync = promisify(execFile);
|
|
82663
83114
|
var NEEDS_SHELL = process.platform === "win32";
|