@stackbone/cli 0.4.1 → 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 +40 -0
- package/main.js +237 -77
- package/package.json +1 -1
- package/stackbone-cli-0.5.0.tgz +0 -0
- package/stackbone-cli-0.4.1.tgz +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-09-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`stackbone dev` records a write's echo and answers `isOwnEcho`.** A
|
|
15
|
+
connector call that names an `echoPath` has the value extracted from the
|
|
16
|
+
provider's response and recorded on its `connection` run step, and the
|
|
17
|
+
emulator plants the reader `isOwnEcho` queries on first dispatch, looking one
|
|
18
|
+
hour back across every workflow in the project. Three cases leave an
|
|
19
|
+
unresolved marker instead of a value, and reading one makes `isOwnEcho` throw
|
|
20
|
+
`echo_guard_unavailable` rather than answer a false "not ours": the path
|
|
21
|
+
resolved to nothing (missing, `null` and `''` are one case, because an empty
|
|
22
|
+
echo would match every later delivery whose value renders empty), the call
|
|
23
|
+
failed in a way that may still have landed (`execute_failed`, `timeout`, an
|
|
24
|
+
envelope-less `credential_error`), or the write ran with no workflow owner,
|
|
25
|
+
such as inside an agent turn's tool, which blinds every caller on that
|
|
26
|
+
connector. The runtime warns which write left the marker when it writes the
|
|
27
|
+
row. A call with no invocation context at all still writes to the provider
|
|
28
|
+
and records nothing, so it warns. Both halves need the `@stackbone/sdk`
|
|
29
|
+
released alongside this version: a project on an older SDK never sends
|
|
30
|
+
`echoPath`, and its `isOwnEcho` throws "No echo guard reader is bound".
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **`stackbone package --target self-host`: the agent no longer races the
|
|
35
|
+
control plane for the shared connect tables.** The two containers share one
|
|
36
|
+
Postgres and both declared `connection_grant`: db-core's migration with a
|
|
37
|
+
bare `CREATE TABLE`, the box's lazy boot DDL with
|
|
38
|
+
`CREATE TABLE IF NOT EXISTS`. Whichever arrived second lost, and a boot where
|
|
39
|
+
the box won killed the control plane's migration and surfaced as a
|
|
40
|
+
health-check timeout. The generated compose now sets
|
|
41
|
+
`STACKBONE_PLATFORM_SCHEMA_OWNER: control-plane` on the agent service and the
|
|
42
|
+
box stands down. Not overridable from `.env`: it is a fact about that compose
|
|
43
|
+
file, not a preference. `stackbone dev` and BYOC set nothing and keep creating
|
|
44
|
+
their own tables.
|
|
45
|
+
|
|
46
|
+
- **A failed broker-context build is no longer cached for the life of the
|
|
47
|
+
process.** One Postgres hiccup on the first Connect request used to disable
|
|
48
|
+
Connect until the container restarted; the next request now rebuilds it.
|
|
49
|
+
|
|
10
50
|
## [0.4.1] - 2026-09-11
|
|
11
51
|
|
|
12
52
|
### Added
|
package/main.js
CHANGED
|
@@ -9720,9 +9720,11 @@ var init_connector_invoker = __esmMin((() => {
|
|
|
9720
9720
|
* @param connector The connect connector id registered in Studio.
|
|
9721
9721
|
* @param operation The operation / tool id to run on that connector.
|
|
9722
9722
|
* @param args The operation arguments (path/query/body params). Defaults `{}`.
|
|
9723
|
-
* @param opts Optional `{ principal, idempotent }`. `principal`
|
|
9724
|
-
* `{ type: 'app' }`; `idempotent` is UNSET by
|
|
9725
|
-
* an explicit `false` changes how a failure is
|
|
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.
|
|
9726
9728
|
*/
|
|
9727
9729
|
async function callConnector(connector, operation, args, opts) {
|
|
9728
9730
|
return (await getConnectorInvoker().invoke({
|
|
@@ -9730,7 +9732,8 @@ async function callConnector(connector, operation, args, opts) {
|
|
|
9730
9732
|
operation,
|
|
9731
9733
|
...args !== void 0 ? { args } : {},
|
|
9732
9734
|
...opts?.principal !== void 0 ? { principal: opts.principal } : {},
|
|
9733
|
-
...opts?.idempotent !== void 0 ? { idempotent: opts.idempotent } : {}
|
|
9735
|
+
...opts?.idempotent !== void 0 ? { idempotent: opts.idempotent } : {},
|
|
9736
|
+
...opts?.echoPath !== void 0 ? { echoPath: opts.echoPath } : {}
|
|
9734
9737
|
})).output;
|
|
9735
9738
|
}
|
|
9736
9739
|
var connectorHandle, connection;
|
|
@@ -14368,6 +14371,29 @@ var createInvocationsStore = (opts) => {
|
|
|
14368
14371
|
]);
|
|
14369
14372
|
return rows[0]?.id ?? null;
|
|
14370
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
|
+
},
|
|
14371
14397
|
async annotatePromptUse(input) {
|
|
14372
14398
|
if (!isUuid(input.runId)) return;
|
|
14373
14399
|
assertOpen();
|
|
@@ -15699,7 +15725,7 @@ var emulatorCors = (allowlist) => cors({
|
|
|
15699
15725
|
var STACKBONE_CLI_BUILD_VERSION, EMULATOR_CAPABILITY_ROUTES, EMULATOR_CAPABILITIES, buildContractResponse;
|
|
15700
15726
|
var init_contract = __esmMin((() => {
|
|
15701
15727
|
init_src$19();
|
|
15702
|
-
STACKBONE_CLI_BUILD_VERSION = "0.
|
|
15728
|
+
STACKBONE_CLI_BUILD_VERSION = "0.5.0";
|
|
15703
15729
|
EMULATOR_CAPABILITY_ROUTES = {
|
|
15704
15730
|
"database.postgres_direct": "/api/db",
|
|
15705
15731
|
"rag.basic": "/api/rag",
|
|
@@ -32303,8 +32329,8 @@ var init_github_connector = __esmMin((() => {
|
|
|
32303
32329
|
triggers: [{
|
|
32304
32330
|
id: "commit-pushed",
|
|
32305
32331
|
displayName: "New commit by date",
|
|
32306
|
-
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.
|
|
32307
|
-
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.",
|
|
32308
32334
|
sampleData: {
|
|
32309
32335
|
sha: "11589d462dc2b29714240e03ce310e7b15aa94c3",
|
|
32310
32336
|
url: "https://github.com/stackbone/stackbone/commit/11589d462dc2b29714240e03ce310e7b15aa94c3",
|
|
@@ -32385,8 +32411,8 @@ var init_github_connector = __esmMin((() => {
|
|
|
32385
32411
|
}, {
|
|
32386
32412
|
id: "issue-updated",
|
|
32387
32413
|
displayName: "Issue updated",
|
|
32388
|
-
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
|
|
32389
|
-
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.",
|
|
32390
32416
|
sampleData: {
|
|
32391
32417
|
issueId: 5320382826,
|
|
32392
32418
|
number: 14317,
|
|
@@ -32782,7 +32808,7 @@ var init_google_drive_connector = __esmMin((() => {
|
|
|
32782
32808
|
post: {
|
|
32783
32809
|
operationId: "drive.files.create",
|
|
32784
32810
|
summary: "Create a folder, or a file with metadata but no content",
|
|
32785
|
-
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.",
|
|
32786
32812
|
"x-stackbone-version-path": "modifiedTime",
|
|
32787
32813
|
parameters: [FIELDS_PARAM, SUPPORTS_ALL_DRIVES_PARAM],
|
|
32788
32814
|
requestBody: {
|
|
@@ -32852,7 +32878,7 @@ var init_google_drive_connector = __esmMin((() => {
|
|
|
32852
32878
|
patch: {
|
|
32853
32879
|
operationId: "drive.files.update",
|
|
32854
32880
|
summary: "Rename a file, move it between folders, or set its metadata",
|
|
32855
|
-
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.",
|
|
32856
32882
|
"x-stackbone-version-path": "modifiedTime",
|
|
32857
32883
|
parameters: [
|
|
32858
32884
|
{
|
|
@@ -33160,8 +33186,8 @@ var init_google_drive_connector = __esmMin((() => {
|
|
|
33160
33186
|
triggers: [{
|
|
33161
33187
|
id: "file-added-to-folder",
|
|
33162
33188
|
displayName: "File added to folder",
|
|
33163
|
-
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
|
|
33164
|
-
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.",
|
|
33165
33191
|
sampleData: {
|
|
33166
33192
|
fileId: "1a2B3cD4eF5gH6iJ7kL8mN9oP",
|
|
33167
33193
|
name: "Weekly sync 2026-08-25.pdf",
|
|
@@ -33381,8 +33407,8 @@ var init_linear_connector = __esmMin((() => {
|
|
|
33381
33407
|
{
|
|
33382
33408
|
id: "issue-created",
|
|
33383
33409
|
displayName: "Issue created",
|
|
33384
|
-
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
|
|
33385
|
-
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.",
|
|
33386
33412
|
sampleData: {
|
|
33387
33413
|
issueId: "a1b2c3d4-0000-0000-0000-000000000000",
|
|
33388
33414
|
identifier: "PLA-812",
|
|
@@ -33471,8 +33497,8 @@ var init_linear_connector = __esmMin((() => {
|
|
|
33471
33497
|
{
|
|
33472
33498
|
id: "comment-created",
|
|
33473
33499
|
displayName: "Comment created",
|
|
33474
|
-
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
|
|
33475
|
-
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.",
|
|
33476
33502
|
sampleData: {
|
|
33477
33503
|
commentId: "e5f6a7b8-0000-0000-0000-000000000000",
|
|
33478
33504
|
body: "Shipped on `next`, closing.",
|
|
@@ -33564,8 +33590,8 @@ var init_linear_connector = __esmMin((() => {
|
|
|
33564
33590
|
{
|
|
33565
33591
|
id: "issue-updated",
|
|
33566
33592
|
displayName: "Issue updated",
|
|
33567
|
-
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.
|
|
33568
|
-
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.",
|
|
33569
33595
|
sampleData: {
|
|
33570
33596
|
issueId: "a1b2c3d4-0000-0000-0000-000000000000",
|
|
33571
33597
|
identifier: "PLA-812",
|
|
@@ -33985,8 +34011,8 @@ var init_supabase_connector = __esmMin((() => {
|
|
|
33985
34011
|
triggers: [{
|
|
33986
34012
|
id: "row-inserted",
|
|
33987
34013
|
displayName: "Row inserted or changed",
|
|
33988
|
-
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
|
|
33989
|
-
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.",
|
|
33990
34016
|
sampleData: {
|
|
33991
34017
|
table: "todos",
|
|
33992
34018
|
row: {
|
|
@@ -51808,8 +51834,27 @@ var CONNECT_DDL = `
|
|
|
51808
51834
|
* Ensure the connect tables exist in the emulator Postgres. Idempotent. Exported
|
|
51809
51835
|
* so the workspace-server can run it once at boot (and the spec can run it before
|
|
51810
51836
|
* exercising the routes) instead of paying the DDL on every request.
|
|
51811
|
-
|
|
51812
|
-
|
|
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;
|
|
51813
51858
|
const client = new Client({ connectionString });
|
|
51814
51859
|
await client.connect();
|
|
51815
51860
|
try {
|
|
@@ -51879,47 +51924,53 @@ var deriveBase64Key = (raw) => createHash("sha256").update(raw).digest().toStrin
|
|
|
51879
51924
|
var makeContextFactory = (opts) => {
|
|
51880
51925
|
let cached;
|
|
51881
51926
|
const get = () => {
|
|
51882
|
-
if (!cached)
|
|
51883
|
-
|
|
51884
|
-
|
|
51885
|
-
|
|
51886
|
-
|
|
51887
|
-
|
|
51888
|
-
|
|
51889
|
-
|
|
51890
|
-
|
|
51891
|
-
|
|
51892
|
-
|
|
51893
|
-
|
|
51894
|
-
|
|
51895
|
-
|
|
51896
|
-
|
|
51897
|
-
|
|
51898
|
-
|
|
51899
|
-
|
|
51900
|
-
|
|
51901
|
-
|
|
51902
|
-
|
|
51903
|
-
|
|
51904
|
-
|
|
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;
|
|
51905
51972
|
});
|
|
51906
|
-
|
|
51907
|
-
db,
|
|
51908
|
-
audit: opts.audit,
|
|
51909
|
-
sql,
|
|
51910
|
-
cipher,
|
|
51911
|
-
store,
|
|
51912
|
-
service,
|
|
51913
|
-
bindings,
|
|
51914
|
-
exchanger,
|
|
51915
|
-
specCache: specCache.cache,
|
|
51916
|
-
brokerCallbackBaseUrl,
|
|
51917
|
-
async close() {
|
|
51918
|
-
await sql.end({ timeout: 5 }).catch(() => void 0);
|
|
51919
|
-
await specCache.close();
|
|
51920
|
-
}
|
|
51921
|
-
};
|
|
51922
|
-
})();
|
|
51973
|
+
}
|
|
51923
51974
|
return cached;
|
|
51924
51975
|
};
|
|
51925
51976
|
const close = async () => {
|
|
@@ -65943,6 +65994,7 @@ var init_load_project_runtime = __esmMin((() => {
|
|
|
65943
65994
|
let setActiveStepReader;
|
|
65944
65995
|
let approvalRequest;
|
|
65945
65996
|
let setConnectorInvoker;
|
|
65997
|
+
let setEchoGuardReader;
|
|
65946
65998
|
let connectorInvoker;
|
|
65947
65999
|
let setActionInvoker;
|
|
65948
66000
|
let actionInvoker;
|
|
@@ -65958,6 +66010,7 @@ var init_load_project_runtime = __esmMin((() => {
|
|
|
65958
66010
|
setApprovalRecorder = sdk.setApprovalRecorder;
|
|
65959
66011
|
setActiveStepReader = sdk.setActiveStepReader;
|
|
65960
66012
|
setConnectorInvoker = sdk.setConnectorInvoker;
|
|
66013
|
+
setEchoGuardReader = sdk.setEchoGuardReader;
|
|
65961
66014
|
connectorInvoker = sdk.createConnectInvoker?.();
|
|
65962
66015
|
setActionInvoker = sdk.setActionInvoker;
|
|
65963
66016
|
actionInvoker = sdk.createActionInvoker?.();
|
|
@@ -66015,6 +66068,7 @@ var init_load_project_runtime = __esmMin((() => {
|
|
|
66015
66068
|
setActiveStepReader,
|
|
66016
66069
|
approvalRequest,
|
|
66017
66070
|
setConnectorInvoker,
|
|
66071
|
+
setEchoGuardReader,
|
|
66018
66072
|
connectorInvoker,
|
|
66019
66073
|
setActionInvoker,
|
|
66020
66074
|
actionInvoker,
|
|
@@ -66679,11 +66733,49 @@ var init_runtime_approval_plants = __esmMin((() => {
|
|
|
66679
66733
|
}));
|
|
66680
66734
|
//#endregion
|
|
66681
66735
|
//#region ../../libs/runtime/workspace-runtime/src/lib/workflow/runtime-connector-plants.ts
|
|
66682
|
-
var plantConnectorInvoker, plantActionInvoker;
|
|
66736
|
+
var owningWorkflow, echoOwner, unresolvedEcho, resolveEcho, echoRecordError, plantConnectorInvoker, plantActionInvoker, ECHO_WINDOW_MS, renderEcho, echoGuardUnavailable, plantEchoGuard;
|
|
66683
66737
|
var init_runtime_connector_plants = __esmMin((() => {
|
|
66738
|
+
init_src$4();
|
|
66739
|
+
init_src$13();
|
|
66684
66740
|
init_call_ordinal();
|
|
66685
66741
|
init_monotonic_span();
|
|
66686
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
|
+
};
|
|
66687
66779
|
plantConnectorInvoker = (rt, ctx) => {
|
|
66688
66780
|
if (!rt.setConnectorInvoker || !rt.connectorInvoker) return;
|
|
66689
66781
|
const baseInvoker = rt.connectorInvoker;
|
|
@@ -66697,14 +66789,15 @@ var init_runtime_connector_plants = __esmMin((() => {
|
|
|
66697
66789
|
const span = startSpan();
|
|
66698
66790
|
const ordinal = stepInfo ? nextConnOrdinal(runId ?? "", stepInfo.stepId, stepInfo.attempt) : 0;
|
|
66699
66791
|
const externalStepId = stepInfo ? `${stepInfo.stepId}::conn::${ordinal}` : randomUUID();
|
|
66700
|
-
const
|
|
66701
|
-
|
|
66792
|
+
const label = `${args.connector}/${args.operation}`;
|
|
66793
|
+
const writeRow = (status, extra, echo) => {
|
|
66794
|
+
if (!runId) return void 0;
|
|
66702
66795
|
const { completedAt, durationMs } = closeSpan(span);
|
|
66703
|
-
recordActivityStep({
|
|
66796
|
+
return recordActivityStep({
|
|
66704
66797
|
runId,
|
|
66705
66798
|
externalStepId,
|
|
66706
66799
|
type: "connection",
|
|
66707
|
-
name:
|
|
66800
|
+
name: label,
|
|
66708
66801
|
status,
|
|
66709
66802
|
startedAt: span.startedAt.toISOString(),
|
|
66710
66803
|
completedAt: completedAt.toISOString(),
|
|
@@ -66712,26 +66805,52 @@ var init_runtime_connector_plants = __esmMin((() => {
|
|
|
66712
66805
|
input: {
|
|
66713
66806
|
connector: args.connector,
|
|
66714
66807
|
operation: args.operation,
|
|
66715
|
-
args: args.args
|
|
66808
|
+
args: args.args,
|
|
66809
|
+
...echo ?? {}
|
|
66716
66810
|
},
|
|
66717
66811
|
...extra,
|
|
66718
66812
|
enclosingExternalStepId: stepInfo?.stepId ?? null
|
|
66719
|
-
})
|
|
66813
|
+
});
|
|
66814
|
+
};
|
|
66815
|
+
const forget = (write) => {
|
|
66816
|
+
write?.catch((err) => {
|
|
66720
66817
|
console.warn(`[connector] run-step write failed for run ${runId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
66721
66818
|
});
|
|
66722
66819
|
};
|
|
66820
|
+
let result;
|
|
66723
66821
|
try {
|
|
66724
|
-
|
|
66725
|
-
record("completed", { output: result.output });
|
|
66726
|
-
return result;
|
|
66822
|
+
result = await baseInvoker.invoke(args);
|
|
66727
66823
|
} catch (err) {
|
|
66728
66824
|
const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
66729
|
-
|
|
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: {
|
|
66730
66828
|
message: err instanceof Error ? err.message : String(err),
|
|
66731
66829
|
...typeof code === "string" ? { code } : {}
|
|
66732
|
-
} });
|
|
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);
|
|
66733
66835
|
throw err;
|
|
66734
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;
|
|
66735
66854
|
} });
|
|
66736
66855
|
} else rt.setConnectorInvoker(baseInvoker);
|
|
66737
66856
|
};
|
|
@@ -66795,6 +66914,34 @@ var init_runtime_connector_plants = __esmMin((() => {
|
|
|
66795
66914
|
}
|
|
66796
66915
|
} });
|
|
66797
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
|
+
};
|
|
66798
66945
|
}));
|
|
66799
66946
|
//#endregion
|
|
66800
66947
|
//#region ../../libs/runtime/workspace-runtime/src/lib/workflow/runtime-sdk-plants.ts
|
|
@@ -66858,6 +67005,7 @@ var init_runtime_sdk_plants = __esmMin((() => {
|
|
|
66858
67005
|
plantPromptUseReporter(rt, ctx);
|
|
66859
67006
|
plantApprovalRecorder(rt, ctx);
|
|
66860
67007
|
plantConnectorInvoker(rt, ctx);
|
|
67008
|
+
plantEchoGuard(rt, ctx);
|
|
66861
67009
|
plantActionInvoker(rt, ctx);
|
|
66862
67010
|
return rt;
|
|
66863
67011
|
};
|
|
@@ -66884,6 +67032,7 @@ var init_runtime_loader = __esmMin((() => {
|
|
|
66884
67032
|
rt.setPromptUseReporter?.(void 0);
|
|
66885
67033
|
rt.setApprovalRecorder?.(void 0);
|
|
66886
67034
|
rt.setConnectorInvoker?.(void 0);
|
|
67035
|
+
rt.setEchoGuardReader?.(void 0);
|
|
66887
67036
|
rt.setActionInvoker?.(void 0);
|
|
66888
67037
|
rt.setActiveStepReader?.(void 0);
|
|
66889
67038
|
planted = void 0;
|
|
@@ -71153,7 +71302,7 @@ var calls_workflow_ts_default = "import { z } from \"zod\";\nimport { streamDeep
|
|
|
71153
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";
|
|
71154
71303
|
//#endregion
|
|
71155
71304
|
//#region src/services/scaffold/template-loader.ts
|
|
71156
|
-
var SDK_TEMPLATE_VERSION = "0.
|
|
71305
|
+
var SDK_TEMPLATE_VERSION = "0.5.0";
|
|
71157
71306
|
var PREFIX = "./templates/";
|
|
71158
71307
|
var templates = new Map(Object.entries(/* @__PURE__ */ Object.assign({
|
|
71159
71308
|
"./templates/deep-agents/index.ts.tmpl": index_ts_default,
|
|
@@ -76178,6 +76327,15 @@ ${objectStore}
|
|
|
76178
76327
|
# MinIO addresses buckets by path. A managed bucket that only answers to the
|
|
76179
76328
|
# bucket-as-subdomain form (AWS S3, Railway Buckets) needs this set to false.
|
|
76180
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
|
|
76181
76339
|
${sharedIdentity}
|
|
76182
76340
|
# REPLACES the agent's built-in origin allowlist — it does not add to it. The
|
|
76183
76341
|
# dashboard is a different ORIGIN from the agent (different port), so its
|
|
@@ -79028,7 +79186,9 @@ function renderOperation(op) {
|
|
|
79028
79186
|
* time (`call-connector.ts`), and no generated signature declared it — so
|
|
79029
79187
|
* `idempotent` was reachable only through the untyped `.call(...)`, which is not
|
|
79030
79188
|
* the path a creator is steered to. Typed as the whole options object rather than
|
|
79031
|
-
* one field,
|
|
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.
|
|
79032
79192
|
*
|
|
79033
79193
|
* It is ALWAYS second, and `renderParam` is what guarantees that: an operation
|
|
79034
79194
|
* with nothing to put in `args` still prints `args?: undefined` ahead of this. The
|
package/package.json
CHANGED
|
Binary file
|
package/stackbone-cli-0.4.1.tgz
DELETED
|
Binary file
|