@stackbone/cli 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/main.js CHANGED
@@ -27,6 +27,7 @@ import { Hono } from "hono";
27
27
  import { cors } from "hono/cors";
28
28
  import Ajv from "ajv/dist/2020.js";
29
29
  import addFormats from "ajv-formats";
30
+ import { createRemoteJWKSet, jwtVerify } from "jose";
30
31
  import { streamSSE } from "hono/streaming";
31
32
  import PgQueryFactory from "pg-query-emscripten";
32
33
  import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
@@ -473,7 +474,7 @@ function requireVersion(contract) {
473
474
  * never falls through to a misleading "capability missing" message when the
474
475
  * actual problem is a stale datapath. Pure, never throws.
475
476
  */
476
- function requireCapability(capability, contract) {
477
+ function requireCapability$1(capability, contract) {
477
478
  const versionCheck = requireVersion(contract);
478
479
  if (versionCheck.error) return versionCheck;
479
480
  if (!contract.capabilities.includes(capability)) return {
@@ -507,6 +508,7 @@ var init_contract = __esmMin((() => {
507
508
  "ai.openrouter",
508
509
  "prompts.basic"
509
510
  ];
511
+ new Set(CAPABILITIES);
510
512
  MODULE_CAPABILITIES = {
511
513
  database: "database.postgres_direct",
512
514
  rag: "rag.basic",
@@ -522,7 +524,7 @@ var init_contract = __esmMin((() => {
522
524
  contractResponseSchema = z.object({
523
525
  version: z.number().int().min(1),
524
526
  minSupported: z.number().int().min(1),
525
- capabilities: z.array(z.enum(CAPABILITIES)),
527
+ capabilities: z.array(z.string()),
526
528
  build: z.object({
527
529
  name: contractBuildNameSchema,
528
530
  version: z.string().min(1)
@@ -2777,7 +2779,7 @@ var init_contract_handshake = __esmMin((() => {
2777
2779
  */
2778
2780
  function assertCapability(module, contract) {
2779
2781
  const capability = MODULE_CAPABILITIES[module];
2780
- const result = requireCapability(capability, contract);
2782
+ const result = requireCapability$1(capability, contract);
2781
2783
  if (result.error) return err$1(result.error);
2782
2784
  return {
2783
2785
  data: void 0,
@@ -5503,11 +5505,11 @@ function isUnsupportedModelError(error) {
5503
5505
  if (error.code === "ai_validation_error") return true;
5504
5506
  return false;
5505
5507
  }
5506
- var DEFAULT_BATCH_SIZE$1, DEFAULT_CONCURRENCY, Embedder;
5508
+ var DEFAULT_BATCH_SIZE$1, DEFAULT_CONCURRENCY$1, Embedder;
5507
5509
  var init_embedder = __esmMin((() => {
5508
5510
  init_result$1();
5509
5511
  DEFAULT_BATCH_SIZE$1 = 128;
5510
- DEFAULT_CONCURRENCY = 4;
5512
+ DEFAULT_CONCURRENCY$1 = 4;
5511
5513
  Embedder = class {
5512
5514
  constructor(options) {
5513
5515
  const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE$1;
@@ -5515,7 +5517,7 @@ var init_embedder = __esmMin((() => {
5515
5517
  this._ai = options.ai;
5516
5518
  this._model = options.model;
5517
5519
  this._batchSize = batchSize;
5518
- this._concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
5520
+ this._concurrency = options.concurrency ?? DEFAULT_CONCURRENCY$1;
5519
5521
  }
5520
5522
  get model() {
5521
5523
  return this._model;
@@ -8744,6 +8746,169 @@ var createSessionsStore = (opts) => {
8744
8746
  };
8745
8747
  };
8746
8748
  //#endregion
8749
+ //#region ../../libs/runtime/workspace-runtime/src/lib/connect-inbound/trigger-subscriptions-store.ts
8750
+ init_store_pool();
8751
+ init_uuid();
8752
+ var SUBSCRIPTION_COLUMNS = "id, connector, trigger_id, principal_key, workflow_name, mapping, mapping_graph, correlation_key, enabled, mailbox";
8753
+ var rowToSubscription = (row) => ({
8754
+ id: row.id,
8755
+ connector: row.connector,
8756
+ trigger: row.trigger_id,
8757
+ principalKey: row.principal_key,
8758
+ workflowName: row.workflow_name,
8759
+ mapping: row.mapping,
8760
+ mappingGraph: row.mapping_graph ?? null,
8761
+ correlationKey: row.correlation_key ?? null,
8762
+ enabled: row.enabled,
8763
+ mailbox: row.mailbox ?? null
8764
+ });
8765
+ var createTriggerSubscriptionsStore = (opts) => {
8766
+ const { pool, assertOpen, close } = createStorePool("TriggerSubscriptionsStore", opts.connectionString);
8767
+ return {
8768
+ async register(input) {
8769
+ assertOpen();
8770
+ const id = randomUUID();
8771
+ const { rows } = await pool.query(`INSERT INTO stackbone_platform.trigger_subscriptions
8772
+ (id, connector, trigger_id, principal_key, workflow_name, mapping, mapping_graph, correlation_key, enabled, mailbox)
8773
+ VALUES ($1::uuid, $2, $3, $4, $5, $6, $7::jsonb, $8, $9, $10)
8774
+ RETURNING ${SUBSCRIPTION_COLUMNS}`, [
8775
+ id,
8776
+ input.connector,
8777
+ input.trigger,
8778
+ input.principalKey,
8779
+ input.workflowName,
8780
+ input.mapping ?? null,
8781
+ input.mappingGraph == null ? null : JSON.stringify(input.mappingGraph),
8782
+ input.correlationKey ?? null,
8783
+ input.enabled ?? true,
8784
+ input.mailbox ?? null
8785
+ ]);
8786
+ return rowToSubscription(rows[0]);
8787
+ },
8788
+ async list() {
8789
+ assertOpen();
8790
+ const { rows } = await pool.query(`SELECT ${SUBSCRIPTION_COLUMNS}
8791
+ FROM stackbone_platform.trigger_subscriptions
8792
+ ORDER BY created_at DESC, id DESC`);
8793
+ return rows.map(rowToSubscription);
8794
+ },
8795
+ async get(id) {
8796
+ if (!isUuid(id)) return null;
8797
+ assertOpen();
8798
+ const { rows } = await pool.query(`SELECT ${SUBSCRIPTION_COLUMNS}
8799
+ FROM stackbone_platform.trigger_subscriptions
8800
+ WHERE id = $1::uuid`, [id]);
8801
+ return rows[0] ? rowToSubscription(rows[0]) : null;
8802
+ },
8803
+ async update(id, patch) {
8804
+ if (!isUuid(id)) return null;
8805
+ assertOpen();
8806
+ const sets = [];
8807
+ const params = [id];
8808
+ if (patch.enabled !== void 0) {
8809
+ params.push(patch.enabled);
8810
+ sets.push(`enabled = $${params.length}`);
8811
+ }
8812
+ if (patch.mapping !== void 0) {
8813
+ params.push(patch.mapping);
8814
+ sets.push(`mapping = $${params.length}`);
8815
+ }
8816
+ if (patch.mappingGraph !== void 0) {
8817
+ params.push(patch.mappingGraph == null ? null : JSON.stringify(patch.mappingGraph));
8818
+ sets.push(`mapping_graph = $${params.length}::jsonb`);
8819
+ }
8820
+ if (patch.correlationKey !== void 0) {
8821
+ params.push(patch.correlationKey);
8822
+ sets.push(`correlation_key = $${params.length}`);
8823
+ }
8824
+ if (sets.length === 0) return this.get(id);
8825
+ const { rows } = await pool.query(`UPDATE stackbone_platform.trigger_subscriptions
8826
+ SET ${sets.join(", ")}, updated_at = now()
8827
+ WHERE id = $1::uuid
8828
+ RETURNING ${SUBSCRIPTION_COLUMNS}`, params);
8829
+ return rows[0] ? rowToSubscription(rows[0]) : null;
8830
+ },
8831
+ async remove(id) {
8832
+ if (!isUuid(id)) return false;
8833
+ assertOpen();
8834
+ await pool.query("DELETE FROM stackbone_platform.trigger_deliveries WHERE subscription_id = $1::uuid", [id]);
8835
+ return ((await pool.query("DELETE FROM stackbone_platform.trigger_subscriptions WHERE id = $1::uuid", [id])).rowCount ?? 0) > 0;
8836
+ },
8837
+ async listActive() {
8838
+ assertOpen();
8839
+ const { rows } = await pool.query(`SELECT ${SUBSCRIPTION_COLUMNS}
8840
+ FROM stackbone_platform.trigger_subscriptions
8841
+ WHERE enabled
8842
+ ORDER BY created_at ASC, id ASC`);
8843
+ return rows.map(rowToSubscription);
8844
+ },
8845
+ async readCursor(subscriptionId) {
8846
+ assertOpen();
8847
+ const { rows } = await pool.query("SELECT poll_cursor FROM stackbone_platform.trigger_subscriptions WHERE id = $1::uuid", [subscriptionId]);
8848
+ return rows[0]?.poll_cursor ?? null;
8849
+ },
8850
+ async saveCursor({ subscriptionId, cursor, lastPolledAt }) {
8851
+ assertOpen();
8852
+ await pool.query(`UPDATE stackbone_platform.trigger_subscriptions
8853
+ SET poll_cursor = $2::jsonb, last_polled_at = $3::timestamptz, updated_at = now()
8854
+ WHERE id = $1::uuid`, [
8855
+ subscriptionId,
8856
+ cursor == null ? null : JSON.stringify(cursor),
8857
+ lastPolledAt.toISOString()
8858
+ ]);
8859
+ },
8860
+ async claim(subscriptionId, providerItemId) {
8861
+ assertOpen();
8862
+ const { rows } = await pool.query(`INSERT INTO stackbone_platform.trigger_deliveries
8863
+ (id, subscription_id, provider_item_id, status)
8864
+ VALUES ($1::uuid, $2::uuid, $3, 'claimed')
8865
+ ON CONFLICT (subscription_id, provider_item_id) DO NOTHING
8866
+ RETURNING id`, [
8867
+ randomUUID(),
8868
+ subscriptionId,
8869
+ providerItemId
8870
+ ]);
8871
+ return rows.length > 0;
8872
+ },
8873
+ async markStarted(subscriptionId, providerItemId, runId) {
8874
+ assertOpen();
8875
+ await pool.query(`UPDATE stackbone_platform.trigger_deliveries
8876
+ SET status = 'started', run_id = $3, updated_at = now()
8877
+ WHERE subscription_id = $1::uuid AND provider_item_id = $2`, [
8878
+ subscriptionId,
8879
+ providerItemId,
8880
+ runId
8881
+ ]);
8882
+ },
8883
+ async markResumed(subscriptionId, providerItemId, runId) {
8884
+ assertOpen();
8885
+ await pool.query(`UPDATE stackbone_platform.trigger_deliveries
8886
+ SET status = 'resumed', run_id = COALESCE($3, run_id), updated_at = now()
8887
+ WHERE subscription_id = $1::uuid AND provider_item_id = $2`, [
8888
+ subscriptionId,
8889
+ providerItemId,
8890
+ runId ?? null
8891
+ ]);
8892
+ },
8893
+ async markRejected(subscriptionId, providerItemId, issues) {
8894
+ assertOpen();
8895
+ await pool.query(`UPDATE stackbone_platform.trigger_deliveries
8896
+ SET status = 'rejected', issues = $3::jsonb, updated_at = now()
8897
+ WHERE subscription_id = $1::uuid AND provider_item_id = $2`, [
8898
+ subscriptionId,
8899
+ providerItemId,
8900
+ JSON.stringify(issues)
8901
+ ]);
8902
+ },
8903
+ async release(subscriptionId, providerItemId) {
8904
+ assertOpen();
8905
+ await pool.query(`DELETE FROM stackbone_platform.trigger_deliveries
8906
+ WHERE subscription_id = $1::uuid AND provider_item_id = $2`, [subscriptionId, providerItemId]);
8907
+ },
8908
+ close
8909
+ };
8910
+ };
8911
+ //#endregion
8747
8912
  //#region ../../libs/runtime/workspace-runtime/src/lib/services/event-bus.ts
8748
8913
  var createEventBus = () => {
8749
8914
  const listeners = /* @__PURE__ */ new Set();
@@ -9003,21 +9168,26 @@ var deepEqual$1 = (a, b) => {
9003
9168
  return aKeys.every((key) => deepEqual$1(a[key], b[key]));
9004
9169
  };
9005
9170
  //#endregion
9006
- //#region ../../libs/runtime/workspace-runtime/src/lib/services/dev-hmac-secret.ts
9007
- var DEV_HMAC_SECRET = "dev-stackbone-local-emulator-hmac-secret";
9008
- //#endregion
9009
9171
  //#region ../../libs/runtime/workspace-runtime/src/lib/server/request-signature-middleware.ts
9010
9172
  init_src$11();
9173
+ var PROXY_CHALLENGE_PATH = "/api/proxy-challenge";
9011
9174
  var DEFAULT_PUBLIC_PREFIXES = [
9012
9175
  "/api/contract",
9013
9176
  "/api/health",
9014
9177
  "/live",
9015
9178
  "/health",
9016
- "/api/connect",
9179
+ PROXY_CHALLENGE_PATH,
9180
+ "/api/connect/callback",
9181
+ "/api/connect/connected",
9182
+ "/api/connect/token",
9183
+ "/api/connect/execute",
9184
+ "/api/connect/evict",
9017
9185
  "/connect/v1",
9018
9186
  "/openai",
9019
9187
  "/anthropic",
9020
- "/agui"
9188
+ "/agui",
9189
+ "/.well-known/workflow/v1",
9190
+ "/api/workflows/hooks"
9021
9191
  ];
9022
9192
  var isPublicPath = (pathname, prefixes) => {
9023
9193
  if (pathname === "/") return true;
@@ -9054,9 +9224,73 @@ var createRequestSignatureVerifier = (options) => {
9054
9224
  };
9055
9225
  };
9056
9226
  //#endregion
9227
+ //#region ../../libs/runtime/workspace-runtime/src/lib/server/jwt-verifier-middleware.ts
9228
+ var JWT_DEFAULT_PUBLIC_PREFIXES = DEFAULT_PUBLIC_PREFIXES;
9229
+ var JWKS_COOLDOWN_MS = 3e4;
9230
+ var JWKS_CACHE_MAX_AGE_MS = 10 * 6e4;
9231
+ /** The Hono context variable the verified identity is stashed under. */
9232
+ var VERIFIED_IDENTITY_VAR = "stackboneVerifiedIdentity";
9233
+ /** Read the identity a successful JWT verification stashed on the context. */
9234
+ var readVerifiedIdentity = (c) => c.get(VERIFIED_IDENTITY_VAR);
9235
+ var BEARER_RE = /^Bearer\s+(.+)$/i;
9236
+ var extractBearer = (header) => {
9237
+ if (!header) return null;
9238
+ return BEARER_RE.exec(header.trim())?.[1]?.trim() || null;
9239
+ };
9240
+ var extractIdentity = (payload) => {
9241
+ const userId = typeof payload.sub === "string" && payload.sub ? payload.sub : null;
9242
+ if (!userId) return null;
9243
+ const org = typeof payload["org"] === "string" ? payload["org"] : void 0;
9244
+ const role = typeof payload["role"] === "string" ? payload["role"] : void 0;
9245
+ const install = typeof payload["install"] === "string" ? payload["install"] : void 0;
9246
+ return {
9247
+ userId,
9248
+ ...org !== void 0 ? { org } : {},
9249
+ ...role !== void 0 ? { role } : {},
9250
+ ...install !== void 0 ? { install } : {}
9251
+ };
9252
+ };
9253
+ /**
9254
+ * Build the Hono middleware that gates the box's surface behind a valid asymmetric
9255
+ * identity JWT. Mount it right after CORS (like the HMAC verifier) so the preflight
9256
+ * is answered by `hono/cors` and every gated route sees a verified caller.
9257
+ */
9258
+ var createJwtVerifier = (options) => {
9259
+ const publicPrefixes = options.publicPrefixes ?? JWT_DEFAULT_PUBLIC_PREFIXES;
9260
+ const getKey = options.createJwks?.() ?? createRemoteJWKSet(new URL(options.jwksUrl), {
9261
+ cooldownDuration: JWKS_COOLDOWN_MS,
9262
+ cacheMaxAge: JWKS_CACHE_MAX_AGE_MS
9263
+ });
9264
+ const reject = (c) => c.json({
9265
+ code: "invalid_token",
9266
+ message: "Identity token is missing, expired, or invalid."
9267
+ }, 401);
9268
+ return async (c, next) => {
9269
+ if (c.req.method === "OPTIONS") return next();
9270
+ if (isPublicPath(new URL(c.req.url).pathname, publicPrefixes)) return next();
9271
+ const token = extractBearer(c.req.header("authorization"));
9272
+ if (!token) return reject(c);
9273
+ let payload;
9274
+ try {
9275
+ ({payload} = await jwtVerify(token, getKey, {
9276
+ issuer: options.issuer,
9277
+ audience: options.audience
9278
+ }));
9279
+ } catch {
9280
+ return reject(c);
9281
+ }
9282
+ const identity = extractIdentity(payload);
9283
+ if (!identity) return reject(c);
9284
+ if (identity.org !== options.expectedOrg) return reject(c);
9285
+ if (identity.install !== void 0 && identity.install !== options.expectedInstall) return reject(c);
9286
+ c.set(VERIFIED_IDENTITY_VAR, identity);
9287
+ return next();
9288
+ };
9289
+ };
9290
+ //#endregion
9057
9291
  //#region ../../libs/runtime/workspace-runtime/src/lib/server/contract.ts
9058
9292
  init_src$13();
9059
- var STACKBONE_CLI_BUILD_VERSION = "0.2.7";
9293
+ var STACKBONE_CLI_BUILD_VERSION = "0.2.8";
9060
9294
  /**
9061
9295
  * Capabilities the local emulator advertises on `GET /api/contract`, derived
9062
9296
  * from the route registry above so the advertised set and the mounted routes
@@ -9698,9 +9932,63 @@ var init_src$6 = __esmMin((() => {
9698
9932
  //#region ../../libs/runtime/workspace-runtime/src/lib/handlers/actor.ts
9699
9933
  init_src$11();
9700
9934
  function readActorUserId(c) {
9935
+ const verified = readVerifiedIdentity(c);
9936
+ if (verified?.userId) return verified.userId;
9701
9937
  return c.req.header("x-stackbone-actor-user-id") || null;
9702
9938
  }
9703
9939
  //#endregion
9940
+ //#region ../../libs/runtime/workspace-runtime/src/lib/handlers/require-capability.ts
9941
+ var ROLES_WITH_CAPABILITY = {
9942
+ "config:write": new Set([
9943
+ "owner",
9944
+ "admin",
9945
+ "member"
9946
+ ]),
9947
+ "hitl:decide": new Set([
9948
+ "owner",
9949
+ "admin",
9950
+ "approver"
9951
+ ]),
9952
+ "rag:write": new Set([
9953
+ "owner",
9954
+ "admin",
9955
+ "member"
9956
+ ]),
9957
+ "runs:invoke": new Set([
9958
+ "owner",
9959
+ "admin",
9960
+ "member"
9961
+ ]),
9962
+ "db:query": new Set([
9963
+ "owner",
9964
+ "admin",
9965
+ "member"
9966
+ ]),
9967
+ "secrets:rotate": new Set(["owner", "admin"]),
9968
+ "secrets:reveal": new Set(["owner", "admin"]),
9969
+ "connections:manage": new Set(["owner", "admin"])
9970
+ };
9971
+ /**
9972
+ * Build a Hono middleware that requires the caller's verified identity to grant
9973
+ * `capability`. A pass-through when there is no verified identity (proxy / dev /
9974
+ * self-host datapaths — see the module comment). On the browser-direct datapath a
9975
+ * missing/unknown role, or a role the matrix does not grant the capability, is a
9976
+ * 403 (fail-closed) before the handler runs.
9977
+ */
9978
+ var requireCapability = (capability) => {
9979
+ const allowed = ROLES_WITH_CAPABILITY[capability];
9980
+ return async (c, next) => {
9981
+ const identity = readVerifiedIdentity(c);
9982
+ if (!identity) return next();
9983
+ const role = identity.role;
9984
+ if (!role || !allowed.has(role)) return c.json({
9985
+ code: "forbidden",
9986
+ message: `This action requires the ${capability} capability.`
9987
+ }, 403);
9988
+ return next();
9989
+ };
9990
+ };
9991
+ //#endregion
9704
9992
  //#region ../../libs/runtime/workspace-runtime/src/lib/handlers/prompts-handlers.ts
9705
9993
  init_src$10();
9706
9994
  init_src$13();
@@ -9800,7 +10088,7 @@ var createPromptsRouter = (opts) => {
9800
10088
  ORDER BY p.key
9801
10089
  LIMIT 200`)).rows.map((row) => rowToPrompt(row, row.current_version, row.content)) };
9802
10090
  })));
9803
- app.post("/", async (c) => {
10091
+ app.post("/", requireCapability("config:write"), async (c) => {
9804
10092
  const raw = await c.req.json().catch(() => ({}));
9805
10093
  const parsed = createStudioPromptRequestSchema.safeParse(raw);
9806
10094
  if (!parsed.success) return c.json(invalidPayload$1(parsed.error.issues), 400);
@@ -9864,7 +10152,7 @@ var createPromptsRouter = (opts) => {
9864
10152
  return c.json(rowToPrompt(head, head.current_version, head.content));
9865
10153
  });
9866
10154
  });
9867
- app.put("/:key", async (c) => {
10155
+ app.put("/:key", requireCapability("config:write"), async (c) => {
9868
10156
  const keyParsed = studioPromptKeyParamSchema.safeParse(c.req.param("key"));
9869
10157
  if (!keyParsed.success) return c.json(notFound, 404);
9870
10158
  const raw = await c.req.json().catch(() => ({}));
@@ -9897,7 +10185,7 @@ var createPromptsRouter = (opts) => {
9897
10185
  return updated ? c.json(rowToPrompt(updated, updated.current_version, updated.content)) : c.json(notFound, 404);
9898
10186
  });
9899
10187
  });
9900
- app.delete("/:key", async (c) => {
10188
+ app.delete("/:key", requireCapability("config:write"), async (c) => {
9901
10189
  const keyParsed = studioPromptKeyParamSchema.safeParse(c.req.param("key"));
9902
10190
  if (!keyParsed.success) return c.json(notFound, 404);
9903
10191
  return withClient$2(opts, async (client) => {
@@ -9924,7 +10212,7 @@ var createPromptsRouter = (opts) => {
9924
10212
  return c.json({ items: res.rows.map(rowToVersionItem) });
9925
10213
  });
9926
10214
  });
9927
- app.post("/:key/rollback", async (c) => {
10215
+ app.post("/:key/rollback", requireCapability("config:write"), async (c) => {
9928
10216
  const keyParsed = studioPromptKeyParamSchema.safeParse(c.req.param("key"));
9929
10217
  if (!keyParsed.success) return c.json(notFound, 404);
9930
10218
  const raw = await c.req.json().catch(() => ({}));
@@ -10169,7 +10457,7 @@ var createStorageRouter = (storageOptions) => {
10169
10457
  const response = await listObjects$1(storageOptions, bucketParsed.data, queryParsed.data);
10170
10458
  return c.json(response);
10171
10459
  });
10172
- app.post("/buckets/:bucket/upload", async (c) => {
10460
+ app.post("/buckets/:bucket/upload", requireCapability("runs:invoke"), async (c) => {
10173
10461
  const bucketParsed = studioStorageBucketParamSchema.safeParse(c.req.param("bucket"));
10174
10462
  if (!bucketParsed.success) return c.json({
10175
10463
  code: "bucket_not_found",
@@ -10199,7 +10487,7 @@ var createStorageRouter = (storageOptions) => {
10199
10487
  const response = await uploadObject$1(storageOptions, bucketParsed.data, keyParsed.data, buffer, file.type || void 0);
10200
10488
  return c.json(response);
10201
10489
  });
10202
- app.post("/buckets/:bucket/folders", async (c) => {
10490
+ app.post("/buckets/:bucket/folders", requireCapability("runs:invoke"), async (c) => {
10203
10491
  const bucketParsed = studioStorageBucketParamSchema.safeParse(c.req.param("bucket"));
10204
10492
  if (!bucketParsed.success) return c.json({
10205
10493
  code: "bucket_not_found",
@@ -10227,7 +10515,7 @@ var createStorageRouter = (storageOptions) => {
10227
10515
  throw err;
10228
10516
  }
10229
10517
  });
10230
- app.delete("/buckets/:bucket/objects/:key", async (c) => {
10518
+ app.delete("/buckets/:bucket/objects/:key", requireCapability("runs:invoke"), async (c) => {
10231
10519
  const bucketParsed = studioStorageBucketParamSchema.safeParse(c.req.param("bucket"));
10232
10520
  if (!bucketParsed.success) return c.json({
10233
10521
  code: "bucket_not_found",
@@ -10414,11 +10702,11 @@ init_src$13();
10414
10702
  */
10415
10703
  var createSecretsRouter = (secretsOptions) => {
10416
10704
  const app = new Hono();
10417
- app.get("/", async (c) => {
10705
+ app.get("/", requireCapability("secrets:rotate"), async (c) => {
10418
10706
  const response = await listSecrets$1(secretsOptions);
10419
10707
  return c.json(response);
10420
10708
  });
10421
- app.post("/", async (c) => {
10709
+ app.post("/", requireCapability("secrets:rotate"), async (c) => {
10422
10710
  const raw = await c.req.json().catch(() => ({}));
10423
10711
  const parsed = createStudioSecretRequestSchema.safeParse(raw);
10424
10712
  if (!parsed.success) return c.json({
@@ -10437,7 +10725,7 @@ var createSecretsRouter = (secretsOptions) => {
10437
10725
  throw err;
10438
10726
  }
10439
10727
  });
10440
- app.delete("/:name", async (c) => {
10728
+ app.delete("/:name", requireCapability("secrets:rotate"), async (c) => {
10441
10729
  const parsed = studioSecretNameParamSchema.safeParse(c.req.param("name"));
10442
10730
  if (!parsed.success) return c.json({
10443
10731
  code: "studio_secret_not_found",
@@ -10461,7 +10749,7 @@ var createSecretsRouter = (secretsOptions) => {
10461
10749
  throw err;
10462
10750
  }
10463
10751
  });
10464
- app.post("/:name/reveal", async (c) => {
10752
+ app.post("/:name/reveal", requireCapability("secrets:reveal"), async (c) => {
10465
10753
  const parsed = studioSecretNameParamSchema.safeParse(c.req.param("name"));
10466
10754
  if (!parsed.success) return c.json({
10467
10755
  code: "studio_secret_not_found",
@@ -10684,7 +10972,7 @@ var createConfigRouter = (agentConfigOptions) => {
10684
10972
  const response = await getAgentConfig(agentConfigOptions);
10685
10973
  return c.json(response);
10686
10974
  });
10687
- app.put("/", async (c) => {
10975
+ app.put("/", requireCapability("config:write"), async (c) => {
10688
10976
  const raw = await c.req.json().catch(() => ({}));
10689
10977
  const parsed = studioAgentConfigPutRequestSchema.safeParse(raw);
10690
10978
  if (!parsed.success) return c.json({
@@ -10708,7 +10996,7 @@ var createConfigRouter = (agentConfigOptions) => {
10708
10996
  const response = await listAgentConfigVersions(agentConfigOptions);
10709
10997
  return c.json(response);
10710
10998
  });
10711
- app.post("/rollback/:version", async (c) => {
10999
+ app.post("/rollback/:version", requireCapability("config:write"), async (c) => {
10712
11000
  const parsed = studioAgentConfigVersionParamSchema.safeParse(c.req.param("version"));
10713
11001
  if (!parsed.success) return c.json({
10714
11002
  code: "agent_config_version_not_found",
@@ -11110,7 +11398,7 @@ var createRunsRouter = (deps) => {
11110
11398
  throw err;
11111
11399
  }
11112
11400
  });
11113
- app.post("/:id/retry", async (c) => {
11401
+ app.post("/:id/retry", requireCapability("runs:invoke"), async (c) => {
11114
11402
  const result = await deps.invocations.retry(c.req.param("id"));
11115
11403
  if (!result) return c.json({
11116
11404
  code: "run_not_found",
@@ -11131,7 +11419,7 @@ var createRunsRouter = (deps) => {
11131
11419
  });
11132
11420
  return c.json({ run: result.run }, 202);
11133
11421
  });
11134
- app.post("/:id/cancel", async (c) => {
11422
+ app.post("/:id/cancel", requireCapability("runs:invoke"), async (c) => {
11135
11423
  const result = await deps.invocations.cancel(c.req.param("id"));
11136
11424
  if (!result) return c.json({
11137
11425
  code: "run_not_found",
@@ -16587,7 +16875,7 @@ var createApprovalsRouter = (deps) => {
16587
16875
  }, 404);
16588
16876
  return c.json(detail);
16589
16877
  });
16590
- app.post("/:id/decide", async (c) => {
16878
+ app.post("/:id/decide", requireCapability("hitl:decide"), async (c) => {
16591
16879
  const raw = await c.req.json().catch(() => ({}));
16592
16880
  const parsed = decideApprovalRequestSchema.safeParse(raw);
16593
16881
  if (!parsed.success) return c.json({
@@ -16759,11 +17047,11 @@ init_src$13();
16759
17047
  var createDbExplorerRouter = (deps) => {
16760
17048
  const app = new Hono();
16761
17049
  const dbExplorerOptions = { connectionString: deps.connectionString };
16762
- app.get("/schemas", async (c) => {
17050
+ app.get("/schemas", requireCapability("db:query"), async (c) => {
16763
17051
  const response = await listSchemas(dbExplorerOptions);
16764
17052
  return c.json(response);
16765
17053
  });
16766
- app.get("/tables/:schema/:table", async (c) => {
17054
+ app.get("/tables/:schema/:table", requireCapability("db:query"), async (c) => {
16767
17055
  const paramsParsed = dbBrowseParamsSchema.safeParse({
16768
17056
  schema: c.req.param("schema"),
16769
17057
  table: c.req.param("table")
@@ -16797,7 +17085,7 @@ var createDbExplorerRouter = (deps) => {
16797
17085
  throw err;
16798
17086
  }
16799
17087
  });
16800
- app.post("/query", async (c) => {
17088
+ app.post("/query", requireCapability("db:query"), async (c) => {
16801
17089
  const raw = await c.req.json().catch(() => ({}));
16802
17090
  const parsed = dbQueryRequestSchema.safeParse(raw);
16803
17091
  if (!parsed.success) return c.json({
@@ -17010,11 +17298,11 @@ var deleteBrowserProfile = async (opts, origin, identity) => withClient(opts, as
17010
17298
  var createBrowserProfilesRouter = (deps) => {
17011
17299
  const app = new Hono();
17012
17300
  const browserProfilesOptions = { connectionString: deps.connectionString };
17013
- app.get("/", async (c) => {
17301
+ app.get("/", requireCapability("secrets:rotate"), async (c) => {
17014
17302
  const response = await listBrowserProfiles(browserProfilesOptions);
17015
17303
  return c.json(response);
17016
17304
  });
17017
- app.delete("/", async (c) => {
17305
+ app.delete("/", requireCapability("secrets:rotate"), async (c) => {
17018
17306
  const origin = c.req.query("origin");
17019
17307
  const identity = c.req.query("identity") ?? "agent";
17020
17308
  if (!origin) return c.json({
@@ -17973,7 +18261,7 @@ var createRagRouter = (deps) => {
17973
18261
  next_cursor: page.nextCursor
17974
18262
  });
17975
18263
  });
17976
- app.post("/jobs/:jobId/cancel", async (c) => {
18264
+ app.post("/jobs/:jobId/cancel", requireCapability("rag:write"), async (c) => {
17977
18265
  const result = await deps.invocations.cancel(c.req.param("jobId"));
17978
18266
  if (!result) return c.json({
17979
18267
  code: "rag_job_not_found",
@@ -17999,7 +18287,7 @@ var createRagRouter = (deps) => {
17999
18287
  return trigger(input);
18000
18288
  } } : {}
18001
18289
  });
18002
- app.post("/collections", async (c) => {
18290
+ app.post("/collections", requireCapability("rag:write"), async (c) => {
18003
18291
  const raw = await c.req.json().catch(() => ({}));
18004
18292
  const parsed = createRagCollectionRequestSchema.safeParse(raw);
18005
18293
  if (!parsed.success) return c.json({
@@ -18013,7 +18301,7 @@ var createRagRouter = (deps) => {
18013
18301
  return mapRagWriteError(c, err);
18014
18302
  }
18015
18303
  });
18016
- app.delete("/collections/:name", async (c) => {
18304
+ app.delete("/collections/:name", requireCapability("rag:write"), async (c) => {
18017
18305
  const nameParsed = ragCollectionNameParamSchema.safeParse(c.req.param("name"));
18018
18306
  if (!nameParsed.success) return c.json({
18019
18307
  code: "rag_collection_not_found",
@@ -18025,7 +18313,7 @@ var createRagRouter = (deps) => {
18025
18313
  return mapRagWriteError(c, err);
18026
18314
  }
18027
18315
  });
18028
- app.post("/collections/:name/docs", async (c) => {
18316
+ app.post("/collections/:name/docs", requireCapability("rag:write"), async (c) => {
18029
18317
  const nameParsed = ragCollectionNameParamSchema.safeParse(c.req.param("name"));
18030
18318
  if (!nameParsed.success) return c.json({
18031
18319
  code: "rag_collection_not_found",
@@ -18067,7 +18355,7 @@ var createRagRouter = (deps) => {
18067
18355
  return mapRagWriteError(c, err);
18068
18356
  }
18069
18357
  });
18070
- app.delete("/collections/:name/docs/:docId", async (c) => {
18358
+ app.delete("/collections/:name/docs/:docId", requireCapability("rag:write"), async (c) => {
18071
18359
  const nameParsed = ragCollectionNameParamSchema.safeParse(c.req.param("name"));
18072
18360
  if (!nameParsed.success) return c.json({
18073
18361
  code: "rag_collection_not_found",
@@ -18080,7 +18368,7 @@ var createRagRouter = (deps) => {
18080
18368
  return mapRagWriteError(c, err);
18081
18369
  }
18082
18370
  });
18083
- app.post("/collections/:name/docs/:docId/download", async (c) => {
18371
+ app.post("/collections/:name/docs/:docId/download", requireCapability("rag:write"), async (c) => {
18084
18372
  const nameParsed = ragCollectionNameParamSchema.safeParse(c.req.param("name"));
18085
18373
  if (!nameParsed.success) return c.json({
18086
18374
  code: "rag_collection_not_found",
@@ -18093,7 +18381,7 @@ var createRagRouter = (deps) => {
18093
18381
  return mapRagWriteError(c, err);
18094
18382
  }
18095
18383
  });
18096
- app.post("/jobs/:jobId/retry", async (c) => {
18384
+ app.post("/jobs/:jobId/retry", requireCapability("rag:write"), async (c) => {
18097
18385
  const parsed = ragJobIdParamSchema.safeParse({ jobId: c.req.param("jobId") });
18098
18386
  if (!parsed.success) return c.json({
18099
18387
  code: "rag_job_not_found",
@@ -19353,6 +19641,24 @@ var init_drizzle_broker_store = __esmMin((() => {
19353
19641
  async deleteGrant(orgId, connector, principalKey) {
19354
19642
  await this.db.delete(connectionGrantTable).where(and(eq(connectionGrantTable.organizationId, orgId), eq(connectionGrantTable.connector, connector), eq(connectionGrantTable.principalKey, principalKey)));
19355
19643
  }
19644
+ async listRefreshableGrants() {
19645
+ const rows = await this.db.select().from(connectionGrantTable);
19646
+ const grants = [];
19647
+ for (const row of rows) try {
19648
+ grants.push({
19649
+ ...this.decryptGrant(row),
19650
+ healthStatus: row.healthStatus
19651
+ });
19652
+ } catch {}
19653
+ return grants;
19654
+ }
19655
+ async setGrantHealth(input) {
19656
+ await this.db.update(connectionGrantTable).set({
19657
+ healthStatus: input.status,
19658
+ healthDetail: input.detail,
19659
+ updatedAt: /* @__PURE__ */ new Date()
19660
+ }).where(eq(connectionGrantTable.id, input.grantId));
19661
+ }
19356
19662
  encrypt(plaintext) {
19357
19663
  const envelope = this.cipher.encrypt(plaintext);
19358
19664
  return {
@@ -19492,6 +19798,384 @@ var init_default_oauth_exchanger = __esmMin((() => {
19492
19798
  };
19493
19799
  }));
19494
19800
  //#endregion
19801
+ //#region ../../libs/platform/connect-broker/src/lib/refresh/grant-refresh-ports.ts
19802
+ /** Project a decrypted grant onto the refreshable shape the sweep consumes. */
19803
+ function grantToRefreshable(grant) {
19804
+ return {
19805
+ id: grant.id ?? `${grant.orgId}:${grant.connector}:${grant.principalKey}`,
19806
+ organizationId: grant.orgId,
19807
+ connectorId: grant.connector,
19808
+ healthStatus: grant.healthStatus,
19809
+ accessTokenExpiresAt: grant.expiresAt === null ? null : new Date(grant.expiresAt),
19810
+ connector: grant.connector,
19811
+ principalKey: grant.principalKey,
19812
+ hasRefreshToken: grant.refreshToken !== null
19813
+ };
19814
+ }
19815
+ function asHealthStatus(value) {
19816
+ return value;
19817
+ }
19818
+ /**
19819
+ * Whether a refresh failure message signals a PERMANENT grant loss (the grant is
19820
+ * gone and a refresh will never succeed) vs a transient provider/network blip.
19821
+ * OAuth2 returns `invalid_grant` for a revoked/expired refresh token.
19822
+ */
19823
+ function isPermanentRefreshFailure(message) {
19824
+ const lowered = message.toLowerCase();
19825
+ return lowered.includes("invalid_grant") || lowered.includes("invalid grant") || lowered.includes("access_denied") || lowered.includes("unauthorized_client");
19826
+ }
19827
+ var GrantRefreshableConnectionLoader, GrantConnectionRefresher, GrantConnectionHealthWriter, NoopGrantCascadeDisabler;
19828
+ var init_grant_refresh_ports = __esmMin((() => {
19829
+ GrantRefreshableConnectionLoader = class {
19830
+ constructor(lister) {
19831
+ this.lister = lister;
19832
+ }
19833
+ async listRefreshable() {
19834
+ return (await this.lister.listRefreshableGrants()).filter((g) => asHealthStatus(g.healthStatus) !== "revoked").map(grantToRefreshable);
19835
+ }
19836
+ };
19837
+ GrantConnectionRefresher = class {
19838
+ constructor(store, exchanger) {
19839
+ this.store = store;
19840
+ this.exchanger = exchanger;
19841
+ }
19842
+ async refresh(connection) {
19843
+ const grantRef = connection;
19844
+ if (grantRef.hasRefreshToken === false) return {
19845
+ kind: "permanent",
19846
+ reason: "no refresh token"
19847
+ };
19848
+ const grant = await this.store.getGrant(grantRef.organizationId, grantRef.connector, grantRef.principalKey);
19849
+ if (grant === null) return {
19850
+ kind: "permanent",
19851
+ reason: "grant not found"
19852
+ };
19853
+ if (grant.refreshToken === null) return {
19854
+ kind: "permanent",
19855
+ reason: "no refresh token"
19856
+ };
19857
+ const connector = await this.store.getConnector(grant.orgId, grant.connector);
19858
+ if (connector === null) return {
19859
+ kind: "permanent",
19860
+ reason: "connector not registered"
19861
+ };
19862
+ try {
19863
+ const tokens = await this.exchanger.refresh({
19864
+ connector,
19865
+ refreshToken: grant.refreshToken
19866
+ });
19867
+ await this.store.upsertGrant({
19868
+ ...grant,
19869
+ accessToken: tokens.accessToken,
19870
+ refreshToken: tokens.refreshToken ?? grant.refreshToken,
19871
+ expiresAt: tokens.expiresAt ?? null
19872
+ });
19873
+ return { kind: "refreshed" };
19874
+ } catch (err) {
19875
+ const message = err instanceof Error ? err.message : String(err);
19876
+ if (isPermanentRefreshFailure(message)) return {
19877
+ kind: "permanent",
19878
+ reason: message
19879
+ };
19880
+ return {
19881
+ kind: "transient",
19882
+ reason: message
19883
+ };
19884
+ }
19885
+ }
19886
+ };
19887
+ GrantConnectionHealthWriter = class {
19888
+ constructor(setter) {
19889
+ this.setter = setter;
19890
+ }
19891
+ async setHealth(input) {
19892
+ await this.setter.setGrantHealth({
19893
+ grantId: input.connectionId,
19894
+ status: input.status,
19895
+ detail: input.detail
19896
+ });
19897
+ }
19898
+ };
19899
+ NoopGrantCascadeDisabler = class {
19900
+ async disableDependents() {
19901
+ return { disabledAutomationIds: [] };
19902
+ }
19903
+ };
19904
+ }));
19905
+ //#endregion
19906
+ //#region ../../libs/platform/connect-broker/src/lib/refresh/logger.ts
19907
+ var consoleRefreshLogger;
19908
+ var init_logger = __esmMin((() => {
19909
+ consoleRefreshLogger = {
19910
+ error: (message, context) => console.error("ERROR", message, context ? JSON.stringify(context) : ""),
19911
+ warn: (message, context) => console.warn("WARN", message, context ? JSON.stringify(context) : ""),
19912
+ info: (message, context) => console.info("INFO", message, context ? JSON.stringify(context) : ""),
19913
+ debug: (message, context) => console.debug("DEBUG", message, context ? JSON.stringify(context) : "")
19914
+ };
19915
+ })), REFRESH_REVOKED_REASON, ConnectionRefreshProcessor;
19916
+ var init_connection_refresh_processor = __esmMin((() => {
19917
+ init_logger();
19918
+ REFRESH_REVOKED_REASON = "connection token revoked";
19919
+ ConnectionRefreshProcessor = class {
19920
+ constructor(deps) {
19921
+ this.deps = deps;
19922
+ this.leadMs = deps.leadMs ?? 3e5;
19923
+ this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
19924
+ this.logger = deps.logger ?? consoleRefreshLogger;
19925
+ }
19926
+ /**
19927
+ * Decide whether a connection's token is due for a proactive refresh. A NULL
19928
+ * expiry counts as due (PRD: a provider that omitted `expires_in` with no
19929
+ * fallback would otherwise never refresh). Otherwise due when the expiry is
19930
+ * within `leadMs` of now.
19931
+ */
19932
+ isRefreshDue(connection) {
19933
+ if (connection.accessTokenExpiresAt === null) return true;
19934
+ return connection.accessTokenExpiresAt.getTime() - this.now().getTime() <= this.leadMs;
19935
+ }
19936
+ /** Sweep every refreshable connection once. Returns one result per connection. */
19937
+ async sweep() {
19938
+ const connections = await this.deps.connections.listRefreshable();
19939
+ const results = [];
19940
+ for (const connection of connections) results.push(await this.process(connection));
19941
+ return results;
19942
+ }
19943
+ /** Run the state machine for ONE connection. */
19944
+ async process(connection) {
19945
+ if (!this.isRefreshDue(connection)) return {
19946
+ connectionId: connection.id,
19947
+ action: "skipped",
19948
+ reason: "token-valid"
19949
+ };
19950
+ const outcome = await this.deps.refresher.refresh(connection);
19951
+ switch (outcome.kind) {
19952
+ case "refreshed":
19953
+ if (connection.healthStatus !== "active") await this.deps.health.setHealth({
19954
+ connectionId: connection.id,
19955
+ status: "active",
19956
+ detail: null
19957
+ });
19958
+ return {
19959
+ connectionId: connection.id,
19960
+ action: "refreshed"
19961
+ };
19962
+ case "transient":
19963
+ await this.deps.health.setHealth({
19964
+ connectionId: connection.id,
19965
+ status: "error",
19966
+ detail: outcome.reason
19967
+ });
19968
+ return {
19969
+ connectionId: connection.id,
19970
+ action: "transient-failure",
19971
+ reason: outcome.reason
19972
+ };
19973
+ case "permanent": {
19974
+ await this.deps.health.setHealth({
19975
+ connectionId: connection.id,
19976
+ status: "revoked",
19977
+ detail: outcome.reason
19978
+ });
19979
+ const { disabledAutomationIds } = await this.deps.cascade.disableDependents({
19980
+ connectionId: connection.id,
19981
+ reason: REFRESH_REVOKED_REASON
19982
+ });
19983
+ this.logger.warn("oauth refresh: connection revoked, cascaded disable", {
19984
+ connectionId: connection.id,
19985
+ connectorId: connection.connectorId,
19986
+ disabled: disabledAutomationIds.length,
19987
+ reason: outcome.reason
19988
+ });
19989
+ return {
19990
+ connectionId: connection.id,
19991
+ action: "revoked",
19992
+ reason: outcome.reason,
19993
+ disabledAutomationIds
19994
+ };
19995
+ }
19996
+ }
19997
+ }
19998
+ };
19999
+ })), CONNECTION_REFRESH_QUEUE, CONNECTION_REFRESH_JOB_NAME, CONNECTION_REFRESH_SCHEDULER_ID;
20000
+ var init_constants = __esmMin((() => {
20001
+ CONNECTION_REFRESH_QUEUE = "connector-token-refresh";
20002
+ CONNECTION_REFRESH_JOB_NAME = "refresh-sweep";
20003
+ CONNECTION_REFRESH_SCHEDULER_ID = "connector-token-refresh:sweep";
20004
+ }));
20005
+ //#endregion
20006
+ //#region ../../libs/platform/connect-broker/src/lib/refresh/connection-refresh-scheduler.ts
20007
+ var ConnectionRefreshScheduler;
20008
+ var init_connection_refresh_scheduler = __esmMin((() => {
20009
+ init_logger();
20010
+ init_constants();
20011
+ ConnectionRefreshScheduler = class {
20012
+ constructor(options) {
20013
+ this.logger = options.logger ?? consoleRefreshLogger;
20014
+ this.queue = new Queue(CONNECTION_REFRESH_QUEUE, { connection: options.connection });
20015
+ this.queue.on("error", (err) => {
20016
+ this.logger.error(`connection refresh scheduler queue error: ${err.message}`, { stack: err.stack });
20017
+ });
20018
+ this.intervalMs = options.intervalMs ?? 9e5;
20019
+ this.attempts = options.attempts ?? 3;
20020
+ this.backoffDelayMs = options.backoffDelayMs ?? 3e4;
20021
+ }
20022
+ /**
20023
+ * Ensure the single repeatable sweep exists. Idempotent — a re-`ensure()` on
20024
+ * boot upserts the one scheduler in place, so a restart never stacks a second
20025
+ * sweep.
20026
+ */
20027
+ async ensure() {
20028
+ const repeat = { every: this.intervalMs };
20029
+ await this.queue.upsertJobScheduler(CONNECTION_REFRESH_SCHEDULER_ID, repeat, {
20030
+ name: CONNECTION_REFRESH_JOB_NAME,
20031
+ opts: {
20032
+ attempts: this.attempts,
20033
+ backoff: {
20034
+ type: "exponential",
20035
+ delay: this.backoffDelayMs
20036
+ },
20037
+ removeOnComplete: true,
20038
+ removeOnFail: {
20039
+ age: 1440 * 60,
20040
+ count: 1e3
20041
+ }
20042
+ }
20043
+ });
20044
+ }
20045
+ /** Remove the repeatable sweep. Idempotent. */
20046
+ async remove() {
20047
+ return { removed: await this.queue.removeJobScheduler(CONNECTION_REFRESH_SCHEDULER_ID) };
20048
+ }
20049
+ async close() {
20050
+ await this.queue.close();
20051
+ }
20052
+ };
20053
+ }));
20054
+ //#endregion
20055
+ //#region ../../libs/platform/connect-broker/src/lib/refresh/connection-refresh-worker.ts
20056
+ var DEFAULT_CONCURRENCY, ConnectionRefreshWorker;
20057
+ var init_connection_refresh_worker = __esmMin((() => {
20058
+ init_logger();
20059
+ init_constants();
20060
+ DEFAULT_CONCURRENCY = 1;
20061
+ ConnectionRefreshWorker = class {
20062
+ constructor(options) {
20063
+ this.options = options;
20064
+ this.logger = options.logger ?? consoleRefreshLogger;
20065
+ this.worker = new Worker(CONNECTION_REFRESH_QUEUE, async (job) => this.process(job), {
20066
+ connection: options.connection,
20067
+ concurrency: options.concurrency ?? DEFAULT_CONCURRENCY
20068
+ });
20069
+ this.worker.on("error", (err) => {
20070
+ this.logger.error(`connection refresh worker error: ${err.message}`, { stack: err.stack });
20071
+ });
20072
+ this.worker.on("failed", (job, err) => {
20073
+ if (job && job.attemptsMade < (job.opts.attempts ?? 1)) return;
20074
+ this.logger.warn("connection refresh: sweep exhausted", {
20075
+ attempts: job?.attemptsMade ?? null,
20076
+ reason: err.message
20077
+ });
20078
+ });
20079
+ }
20080
+ async waitUntilReady() {
20081
+ await this.worker.waitUntilReady();
20082
+ }
20083
+ async close(force = false) {
20084
+ await this.worker.close(force);
20085
+ }
20086
+ async process(_job) {
20087
+ const results = await this.options.processor.sweep();
20088
+ const revoked = results.filter((r) => r.action === "revoked").length;
20089
+ const transient = results.filter((r) => r.action === "transient-failure").length;
20090
+ const refreshed = results.filter((r) => r.action === "refreshed").length;
20091
+ this.logger.warn("connection refresh: sweep complete", {
20092
+ total: results.length,
20093
+ refreshed,
20094
+ transient,
20095
+ revoked
20096
+ });
20097
+ return results;
20098
+ }
20099
+ };
20100
+ }));
20101
+ //#endregion
20102
+ //#region ../../libs/platform/connect-broker/src/lib/refresh/grant-refresh-runtime.ts
20103
+ /**
20104
+ * Build the broker's grant-backed refresh processor: the generic
20105
+ * `ConnectionRefreshProcessor` with the three grant-backed ports plumbed in. The
20106
+ * caller supplies the store + exchanger (the broker's own seams) and the
20107
+ * store-side listing + health-writing queries.
20108
+ */
20109
+ function createGrantRefreshProcessor(deps) {
20110
+ return new ConnectionRefreshProcessor({
20111
+ connections: new GrantRefreshableConnectionLoader(deps.lister),
20112
+ refresher: new GrantConnectionRefresher(deps.store, deps.exchanger),
20113
+ health: new GrantConnectionHealthWriter(deps.healthSetter),
20114
+ cascade: new NoopGrantCascadeDisabler(),
20115
+ ...deps.leadMs !== void 0 ? { leadMs: deps.leadMs } : {},
20116
+ ...deps.now !== void 0 ? { now: deps.now } : {},
20117
+ ...deps.logger !== void 0 ? { logger: deps.logger } : {}
20118
+ });
20119
+ }
20120
+ /**
20121
+ * Build the broker's refresh SCHEDULER (the single repeatable sweep). A thin
20122
+ * re-instantiation of the connection-refresh scheduler — NOT a fork — so the
20123
+ * cadence / retry / backoff behaviour stays shared.
20124
+ */
20125
+ function createGrantRefreshScheduler(options) {
20126
+ return new ConnectionRefreshScheduler(options);
20127
+ }
20128
+ /**
20129
+ * Build the broker's refresh WORKER, draining the sweep queue and running the
20130
+ * grant-backed processor on each tick. A thin re-instantiation of the
20131
+ * connection-refresh worker.
20132
+ */
20133
+ function createGrantRefreshWorker(options) {
20134
+ return new ConnectionRefreshWorker({
20135
+ connection: options.connection,
20136
+ processor: options.processor,
20137
+ ...options.concurrency !== void 0 ? { concurrency: options.concurrency } : {},
20138
+ ...options.logger !== void 0 ? { logger: options.logger } : {}
20139
+ });
20140
+ }
20141
+ /**
20142
+ * Arm the box's proactive grant-refresh runtime IN ONE CALL (STA-265): build the
20143
+ * single repeatable {@link createGrantRefreshScheduler | sweep scheduler}, start the
20144
+ * {@link createGrantRefreshWorker | worker} that drains it, and `ensure()` the
20145
+ * repeatable exists. Idempotent across boots — the scheduler upserts the ONE
20146
+ * repeatable in place, so a re-arm never stacks a second sweep.
20147
+ *
20148
+ * This is the seam the box composition root (`emulator-server`, which runs the
20149
+ * agent box in cloud BYOC + self-host + the emulator) calls when a Redis URL is
20150
+ * present. The processor is built from the SAME box `DrizzleBrokerStore` the token
20151
+ * hot path uses, so every refresh reads/writes the box `connection_grant` rows —
20152
+ * never the control-plane DB.
20153
+ */
20154
+ async function createGrantRefreshRuntime(deps) {
20155
+ const scheduler = createGrantRefreshScheduler({
20156
+ connection: deps.connection,
20157
+ ...deps.intervalMs !== void 0 ? { intervalMs: deps.intervalMs } : {},
20158
+ ...deps.logger !== void 0 ? { logger: deps.logger } : {}
20159
+ });
20160
+ const worker = createGrantRefreshWorker({
20161
+ connection: deps.connection,
20162
+ processor: deps.processor,
20163
+ ...deps.concurrency !== void 0 ? { concurrency: deps.concurrency } : {},
20164
+ ...deps.logger !== void 0 ? { logger: deps.logger } : {}
20165
+ });
20166
+ await scheduler.ensure();
20167
+ return { async close() {
20168
+ await worker.close().catch(() => void 0);
20169
+ await scheduler.close().catch(() => void 0);
20170
+ } };
20171
+ }
20172
+ var init_grant_refresh_runtime = __esmMin((() => {
20173
+ init_connection_refresh_processor();
20174
+ init_connection_refresh_scheduler();
20175
+ init_connection_refresh_worker();
20176
+ init_grant_refresh_ports();
20177
+ }));
20178
+ //#endregion
19495
20179
  //#region ../../libs/shared/connect-registry/src/lib/triggers/trigger-descriptor.ts
19496
20180
  var triggerCursorSchema, triggerListStepSchema, triggerItemStepSchema, declarativePollSchema, strategyPollSchema, pollSpecSchema, triggerDescriptorSchema;
19497
20181
  var init_trigger_descriptor = __esmMin((() => {
@@ -21043,6 +21727,8 @@ var init_src$2 = __esmMin((() => {
21043
21727
  init_src$4();
21044
21728
  init_src$11();
21045
21729
  init_schema();
21730
+ init_grant_refresh_ports();
21731
+ init_grant_refresh_runtime();
21046
21732
  init_connect_action_executor();
21047
21733
  init_spec_introspection();
21048
21734
  init_connector_definition_from_record();
@@ -21378,6 +22064,7 @@ var makeContextFactory = (opts) => {
21378
22064
  }
21379
22065
  }),
21380
22066
  bindings,
22067
+ exchanger,
21381
22068
  brokerCallbackBaseUrl,
21382
22069
  async close() {
21383
22070
  await sql.end({ timeout: 5 }).catch(() => void 0);
@@ -21398,7 +22085,7 @@ var createConnectRouter = (opts, sharedContext) => {
21398
22085
  const getContext = sharedContext ?? makeContextFactory(opts);
21399
22086
  const registry = opts.registry ?? createRegistryStore();
21400
22087
  const app = new Hono();
21401
- app.post("/connectors", async (c) => {
22088
+ app.post("/connectors", requireCapability("connections:manage"), async (c) => {
21402
22089
  const raw = await c.req.json().catch(() => ({}));
21403
22090
  const parsed = registerConnectorRequestSchema.safeParse(raw);
21404
22091
  if (!parsed.success) return c.json(invalidPayload(parsed.error.issues), 400);
@@ -21434,10 +22121,10 @@ var createConnectRouter = (opts, sharedContext) => {
21434
22121
  displayName: persisted.displayName
21435
22122
  });
21436
22123
  });
21437
- app.get("/catalog", (c) => {
22124
+ app.get("/catalog", requireCapability("connections:manage"), (c) => {
21438
22125
  return c.json({ items: registry.list().map(connectorDefToCatalogEntry) });
21439
22126
  });
21440
- app.get("/connectors", async (c) => {
22127
+ app.get("/connectors", requireCapability("connections:manage"), async (c) => {
21441
22128
  const rows = await (await getContext()).sql`
21442
22129
  SELECT id, provider, connector_id, client_id, client_secret_ciphertext,
21443
22130
  authorize_url, token_url, scopes, auth_mode, placement, display_name
@@ -21446,7 +22133,7 @@ var createConnectRouter = (opts, sharedContext) => {
21446
22133
  ORDER BY provider`;
21447
22134
  return c.json({ items: rows.map((row) => connectorRowToResponse(row, registry)) });
21448
22135
  });
21449
- app.delete("/connectors/:connectorId", async (c) => {
22136
+ app.delete("/connectors/:connectorId", requireCapability("connections:manage"), async (c) => {
21450
22137
  const connectorId = c.req.param("connectorId");
21451
22138
  if ((await (await getContext()).sql`
21452
22139
  DELETE FROM connector
@@ -21460,7 +22147,7 @@ var createConnectRouter = (opts, sharedContext) => {
21460
22147
  status: "deleted"
21461
22148
  });
21462
22149
  });
21463
- app.get("/connectors/:connectorId/operations", async (c) => {
22150
+ app.get("/connectors/:connectorId/operations", requireCapability("connections:manage"), async (c) => {
21464
22151
  const connectorId = c.req.param("connectorId");
21465
22152
  const connector = await (await getContext()).store.getConnector(DEV_ORG_ID, connectorId);
21466
22153
  if (!connector) return c.json({
@@ -21484,7 +22171,7 @@ var createConnectRouter = (opts, sharedContext) => {
21484
22171
  }, 200);
21485
22172
  }
21486
22173
  });
21487
- app.post("/connectors/:connectorId/test", async (c) => {
22174
+ app.post("/connectors/:connectorId/test", requireCapability("connections:manage"), async (c) => {
21488
22175
  const connectorId = c.req.param("connectorId");
21489
22176
  const ctx = await getContext();
21490
22177
  const connector = await ctx.store.getConnector(DEV_ORG_ID, connectorId);
@@ -21515,7 +22202,7 @@ var createConnectRouter = (opts, sharedContext) => {
21515
22202
  }
21516
22203
  });
21517
22204
  app.get("/connected", (c) => c.html("<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\" /><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" /><title>Connected</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;background:#0b0b0c;color:#e7e7ea;display:flex;min-height:100vh;align-items:center;justify-content:center;margin:0}.card{text-align:center;max-width:28rem;padding:2rem}.tick{font-size:2.5rem}h1{font-size:1.1rem;margin:.75rem 0 .25rem}p{color:#a1a1aa;font-size:.9rem;line-height:1.5}</style></head><body><div class=\"card\"><div class=\"tick\">✓</div><h1>Account connected</h1><p>The credential is ready. You can close this tab and return to Studio — it now shows the grant under Connections → Grants.</p></div></body></html>"));
21518
- app.post("/connectors/:connectorId/authorize", async (c) => {
22205
+ app.post("/connectors/:connectorId/authorize", requireCapability("connections:manage"), async (c) => {
21519
22206
  const connectorId = c.req.param("connectorId");
21520
22207
  const body = await c.req.json().catch(() => ({}));
21521
22208
  const agentName = typeof body.agentName === "string" && body.agentName ? body.agentName : void 0;
@@ -21543,7 +22230,7 @@ var createConnectRouter = (opts, sharedContext) => {
21543
22230
  return brokerErrorResponse(c, err);
21544
22231
  }
21545
22232
  });
21546
- app.get("/grants", async (c) => {
22233
+ app.get("/grants", requireCapability("connections:manage"), async (c) => {
21547
22234
  const rows = await (await getContext()).sql`
21548
22235
  SELECT id, connector, principal_key, expires_at, external_subject, health_status
21549
22236
  FROM connection_grant
@@ -21551,7 +22238,7 @@ var createConnectRouter = (opts, sharedContext) => {
21551
22238
  ORDER BY connector, principal_key`;
21552
22239
  return c.json({ items: rows.map(grantRowToResponse) });
21553
22240
  });
21554
- app.delete("/grants/:grantId", async (c) => {
22241
+ app.delete("/grants/:grantId", requireCapability("connections:manage"), async (c) => {
21555
22242
  const grantId = c.req.param("grantId");
21556
22243
  const ctx = await getContext();
21557
22244
  const row = (await ctx.sql`
@@ -21792,13 +22479,23 @@ var createConnectHandlers = (opts) => {
21792
22479
  principal: request.principal
21793
22480
  });
21794
22481
  };
22482
+ const createRefreshProcessor = async () => {
22483
+ const ctx = await shared();
22484
+ return createGrantRefreshProcessor({
22485
+ store: ctx.store,
22486
+ exchanger: ctx.exchanger,
22487
+ lister: { listRefreshableGrants: () => ctx.store.listRefreshableGrants() },
22488
+ healthSetter: { setGrantHealth: (input) => ctx.store.setGrantHealth(input) }
22489
+ });
22490
+ };
21795
22491
  return {
21796
22492
  management: createConnectRouter(opts, shared),
21797
22493
  callback: createConnectCallbackRouter(opts, shared),
21798
22494
  consumerCallback: createConnectConsumerCallbackRouter(opts, shared),
21799
22495
  token: createConnectTokenRouter(opts, shared),
21800
22496
  issueToken,
21801
- evict
22497
+ evict,
22498
+ createRefreshProcessor
21802
22499
  };
21803
22500
  };
21804
22501
  var mergedCallbackParams = async (req) => {
@@ -28806,8 +29503,9 @@ var init_workflow_start_service = __esmMin((() => {
28806
29503
  const resolved = await startResolvedWorkflow(deps, name, input);
28807
29504
  if (resolved.queued) throw new Error(`Cannot await workflow "${name}": it was enqueued behind an active serialized run.`);
28808
29505
  const { run } = resolved;
28809
- if (!run.returnValue) throw new Error(`Cannot await workflow "${name}": the runtime did not expose the run's return value (start() must resolve to a WDK Run with a \`returnValue\`).`);
28810
- const rawOutput = await run.returnValue;
29506
+ const returnValue = run.returnValue;
29507
+ if (!returnValue) throw new Error(`Cannot await workflow "${name}": the runtime did not expose the run's return value (start() must resolve to a WDK Run with a \`returnValue\`).`);
29508
+ const rawOutput = await returnValue;
28811
29509
  const outputValidator = await loadValidator(deps, name, "output");
28812
29510
  if (outputValidator) {
28813
29511
  const verdict = await validateWorkflowInput(outputValidator, rawOutput);
@@ -31595,8 +32293,10 @@ var init_mount = __esmMin((() => {
31595
32293
  //#endregion
31596
32294
  //#region ../../libs/runtime/workspace-runtime/src/lib/server/emulator-server.ts
31597
32295
  init_src$13();
32296
+ init_src$11();
31598
32297
  init_src$9();
31599
32298
  init_src$6();
32299
+ init_src$2();
31600
32300
  init_registry$1();
31601
32301
  init_resume_interrupt();
31602
32302
  /**
@@ -31636,6 +32336,23 @@ var buildEmulatorApp = (opts, hooks = {}) => {
31636
32336
  exposeHeaders: ["X-Stackbone-Run-Id"]
31637
32337
  }));
31638
32338
  if (opts.proxyAuth?.secret) app.use("*", createRequestSignatureVerifier({ secret: opts.proxyAuth.secret }));
32339
+ if (opts.jwtAuth) app.use("*", createJwtVerifier(opts.jwtAuth));
32340
+ if (opts.proxyChallenge?.secret) {
32341
+ const challengeSecret = opts.proxyChallenge.secret;
32342
+ app.get(PROXY_CHALLENGE_PATH, (c) => {
32343
+ const url = new URL(c.req.url);
32344
+ const ok = verifyStackboneSignature({
32345
+ secret: challengeSecret,
32346
+ header: c.req.header(STACKBONE_SIGNATURE_HEADER),
32347
+ payload: proxyRequestPayload({
32348
+ method: "GET",
32349
+ path: url.pathname + url.search,
32350
+ body: ""
32351
+ })
32352
+ });
32353
+ return c.json({ ok }, ok ? 200 : 401);
32354
+ });
32355
+ }
31639
32356
  app.get("/api/contract", (c) => c.json(buildContractResponse()));
31640
32357
  app.get("/", (c) => c.html(renderStudioStub({
31641
32358
  agentSlug: opts.agent.slug,
@@ -31746,12 +32463,15 @@ var buildEmulatorApp = (opts, hooks = {}) => {
31746
32463
  app.route("/api/prompts", createPromptsRouter({ connectionString: opts.injection.services.postgresUrl }));
31747
32464
  const connectHandlers = createConnectHandlers({
31748
32465
  connectionString: opts.injection.services.postgresUrl,
31749
- hmacSecret: DEV_HMAC_SECRET,
32466
+ hmacSecret: opts.connectIdentity?.hmacSecret ?? "dev-stackbone-local-emulator-hmac-secret",
32467
+ ...opts.connectIdentity?.encryptionKey ? { encryptionKey: opts.connectIdentity.encryptionKey } : {},
31750
32468
  ...opts.connectRegistry ? { registry: opts.connectRegistry } : {},
32469
+ ...opts.connectCallbackBaseUrl ? { brokerCallbackBaseUrl: `${opts.connectCallbackBaseUrl.replace(/\/+$/, "")}/api/connect/callback` } : {},
31751
32470
  ...hooks.onWorkflowHookResume ? { onConnectionResume: (token) => hooks.onWorkflowHookResume?.(token, { status: "connected" }) } : {}
31752
32471
  });
31753
32472
  hooks.captureConnectIssueToken?.(connectHandlers.issueToken);
31754
32473
  hooks.captureConnectEvictToken?.(connectHandlers.evict);
32474
+ hooks.captureConnectRefreshProcessorFactory?.(connectHandlers.createRefreshProcessor);
31755
32475
  app.route("/api/connect/callback", connectHandlers.callback);
31756
32476
  app.route("/connect/v1/callback", connectHandlers.consumerCallback);
31757
32477
  app.route("/api/connect", connectHandlers.token);
@@ -31824,7 +32544,7 @@ var startEmulatorServer = async (opts) => {
31824
32544
  body: JSON.stringify(payload ?? {})
31825
32545
  })).catch((err) => console.warn(`[workflow-hook-resume] decide-driven resume for token "${token}" failed: ${err instanceof Error ? err.message : String(err)}`));
31826
32546
  };
31827
- const connectProjectRoot = opts.workflow?.projectRoot;
32547
+ const connectProjectRoot = opts.workflow?.projectRoot ?? opts.deepAgents?.projectRoot;
31828
32548
  const onConnectLog = opts.onConnectLog ?? ((level, message) => console[level === "warn" ? "warn" : "debug"](`[connect-registry] ${message}`));
31829
32549
  const seededRegistry = opts.connectRegistry ?? await seedConnectorRegistry(connectProjectRoot, onConnectLog);
31830
32550
  const seededOpts = {
@@ -31833,6 +32553,8 @@ var startEmulatorServer = async (opts) => {
31833
32553
  };
31834
32554
  let connectIssueToken;
31835
32555
  let connectEvictToken;
32556
+ let connectRefreshProcessorFactory;
32557
+ let grantRefreshRuntime;
31836
32558
  let deepAgentRegistry;
31837
32559
  let deepAgentsProjectRoot;
31838
32560
  const deepAgentLog = opts.onDeepAgentLog ? {
@@ -31871,6 +32593,9 @@ var startEmulatorServer = async (opts) => {
31871
32593
  captureConnectEvictToken: (fn) => {
31872
32594
  connectEvictToken = fn;
31873
32595
  },
32596
+ captureConnectRefreshProcessorFactory: (fn) => {
32597
+ connectRefreshProcessorFactory = fn;
32598
+ },
31874
32599
  getDeepAgentRegistry: () => deepAgentRegistry
31875
32600
  });
31876
32601
  appHolder.app = app;
@@ -31907,6 +32632,19 @@ var startEmulatorServer = async (opts) => {
31907
32632
  };
31908
32633
  serialSupportHolder.support = workflowRuntimeHandle.serialSupport;
31909
32634
  }
32635
+ if (opts.connectRefreshRedisUrl && connectRefreshProcessorFactory) {
32636
+ const redisUrl = opts.connectRefreshRedisUrl;
32637
+ const buildRefreshProcessor = connectRefreshProcessorFactory;
32638
+ try {
32639
+ const processor = await buildRefreshProcessor();
32640
+ grantRefreshRuntime = await createGrantRefreshRuntime({
32641
+ connection: redisConnection(redisUrl),
32642
+ processor
32643
+ });
32644
+ } catch (err) {
32645
+ console.warn(`[grant-refresh] failed to arm the proactive refresh runtime: ${err instanceof Error ? err.message : String(err)}`);
32646
+ }
32647
+ }
31910
32648
  if (!opts.workflow) {
31911
32649
  const fallbackDiscovery = () => ({
31912
32650
  agents: deepAgentManifestEntries(deepAgentRegistry),
@@ -31929,9 +32667,9 @@ var startEmulatorServer = async (opts) => {
31929
32667
  createApproval: (input) => opts.approvals.create(input),
31930
32668
  decideStudioApproval: (threadId, decision) => retireStudioApprovalForThread(opts.approvals, threadId, decision),
31931
32669
  connectTransport: {
31932
- hmacSecret: DEV_HMAC_SECRET,
32670
+ hmacSecret: opts.connectIdentity?.hmacSecret ?? "dev-stackbone-local-emulator-hmac-secret",
31933
32671
  brokerBaseUrl: `http://127.0.0.1:${opts.port}`,
31934
- installationId: DEV_INSTALLATION_ID
32672
+ installationId: opts.connectIdentity?.installationId ?? "local-dev"
31935
32673
  },
31936
32674
  logger: deepAgentLog
31937
32675
  });
@@ -31973,6 +32711,7 @@ var startEmulatorServer = async (opts) => {
31973
32711
  workflowRunSweep?.stop();
31974
32712
  workflowStepUnsubscribe?.();
31975
32713
  await workflowRuntimeHandle?.close().catch(() => void 0);
32714
+ await grantRefreshRuntime?.close().catch(() => void 0);
31976
32715
  if (opts.workflow) await opts.workflow.world.close().catch(() => void 0);
31977
32716
  await deepAgentRegistry?.close().catch(() => void 0);
31978
32717
  await new Promise((resolve) => {
@@ -32024,169 +32763,6 @@ var buildDevServices = (ports) => ({
32024
32763
  var buildDevRedisUrl = (redisPort) => `redis://127.0.0.1:${redisPort}`;
32025
32764
  var DEV_AGENT_SECRET_KEY = createHash("sha256").update(STUDIO_LOCAL_DEV_ENCRYPTION_KEY).digest("base64");
32026
32765
  //#endregion
32027
- //#region ../../libs/runtime/workspace-runtime/src/lib/connect-inbound/trigger-subscriptions-store.ts
32028
- init_store_pool();
32029
- init_uuid();
32030
- var SUBSCRIPTION_COLUMNS = "id, connector, trigger_id, principal_key, workflow_name, mapping, mapping_graph, correlation_key, enabled, mailbox";
32031
- var rowToSubscription = (row) => ({
32032
- id: row.id,
32033
- connector: row.connector,
32034
- trigger: row.trigger_id,
32035
- principalKey: row.principal_key,
32036
- workflowName: row.workflow_name,
32037
- mapping: row.mapping,
32038
- mappingGraph: row.mapping_graph ?? null,
32039
- correlationKey: row.correlation_key ?? null,
32040
- enabled: row.enabled,
32041
- mailbox: row.mailbox ?? null
32042
- });
32043
- var createTriggerSubscriptionsStore = (opts) => {
32044
- const { pool, assertOpen, close } = createStorePool("TriggerSubscriptionsStore", opts.connectionString);
32045
- return {
32046
- async register(input) {
32047
- assertOpen();
32048
- const id = randomUUID();
32049
- const { rows } = await pool.query(`INSERT INTO stackbone_platform.trigger_subscriptions
32050
- (id, connector, trigger_id, principal_key, workflow_name, mapping, mapping_graph, correlation_key, enabled, mailbox)
32051
- VALUES ($1::uuid, $2, $3, $4, $5, $6, $7::jsonb, $8, $9, $10)
32052
- RETURNING ${SUBSCRIPTION_COLUMNS}`, [
32053
- id,
32054
- input.connector,
32055
- input.trigger,
32056
- input.principalKey,
32057
- input.workflowName,
32058
- input.mapping ?? null,
32059
- input.mappingGraph == null ? null : JSON.stringify(input.mappingGraph),
32060
- input.correlationKey ?? null,
32061
- input.enabled ?? true,
32062
- input.mailbox ?? null
32063
- ]);
32064
- return rowToSubscription(rows[0]);
32065
- },
32066
- async list() {
32067
- assertOpen();
32068
- const { rows } = await pool.query(`SELECT ${SUBSCRIPTION_COLUMNS}
32069
- FROM stackbone_platform.trigger_subscriptions
32070
- ORDER BY created_at DESC, id DESC`);
32071
- return rows.map(rowToSubscription);
32072
- },
32073
- async get(id) {
32074
- if (!isUuid(id)) return null;
32075
- assertOpen();
32076
- const { rows } = await pool.query(`SELECT ${SUBSCRIPTION_COLUMNS}
32077
- FROM stackbone_platform.trigger_subscriptions
32078
- WHERE id = $1::uuid`, [id]);
32079
- return rows[0] ? rowToSubscription(rows[0]) : null;
32080
- },
32081
- async update(id, patch) {
32082
- if (!isUuid(id)) return null;
32083
- assertOpen();
32084
- const sets = [];
32085
- const params = [id];
32086
- if (patch.enabled !== void 0) {
32087
- params.push(patch.enabled);
32088
- sets.push(`enabled = $${params.length}`);
32089
- }
32090
- if (patch.mapping !== void 0) {
32091
- params.push(patch.mapping);
32092
- sets.push(`mapping = $${params.length}`);
32093
- }
32094
- if (patch.mappingGraph !== void 0) {
32095
- params.push(patch.mappingGraph == null ? null : JSON.stringify(patch.mappingGraph));
32096
- sets.push(`mapping_graph = $${params.length}::jsonb`);
32097
- }
32098
- if (patch.correlationKey !== void 0) {
32099
- params.push(patch.correlationKey);
32100
- sets.push(`correlation_key = $${params.length}`);
32101
- }
32102
- if (sets.length === 0) return this.get(id);
32103
- const { rows } = await pool.query(`UPDATE stackbone_platform.trigger_subscriptions
32104
- SET ${sets.join(", ")}, updated_at = now()
32105
- WHERE id = $1::uuid
32106
- RETURNING ${SUBSCRIPTION_COLUMNS}`, params);
32107
- return rows[0] ? rowToSubscription(rows[0]) : null;
32108
- },
32109
- async remove(id) {
32110
- if (!isUuid(id)) return false;
32111
- assertOpen();
32112
- await pool.query("DELETE FROM stackbone_platform.trigger_deliveries WHERE subscription_id = $1::uuid", [id]);
32113
- return ((await pool.query("DELETE FROM stackbone_platform.trigger_subscriptions WHERE id = $1::uuid", [id])).rowCount ?? 0) > 0;
32114
- },
32115
- async listActive() {
32116
- assertOpen();
32117
- const { rows } = await pool.query(`SELECT ${SUBSCRIPTION_COLUMNS}
32118
- FROM stackbone_platform.trigger_subscriptions
32119
- WHERE enabled
32120
- ORDER BY created_at ASC, id ASC`);
32121
- return rows.map(rowToSubscription);
32122
- },
32123
- async readCursor(subscriptionId) {
32124
- assertOpen();
32125
- const { rows } = await pool.query("SELECT poll_cursor FROM stackbone_platform.trigger_subscriptions WHERE id = $1::uuid", [subscriptionId]);
32126
- return rows[0]?.poll_cursor ?? null;
32127
- },
32128
- async saveCursor({ subscriptionId, cursor, lastPolledAt }) {
32129
- assertOpen();
32130
- await pool.query(`UPDATE stackbone_platform.trigger_subscriptions
32131
- SET poll_cursor = $2::jsonb, last_polled_at = $3::timestamptz, updated_at = now()
32132
- WHERE id = $1::uuid`, [
32133
- subscriptionId,
32134
- cursor == null ? null : JSON.stringify(cursor),
32135
- lastPolledAt.toISOString()
32136
- ]);
32137
- },
32138
- async claim(subscriptionId, providerItemId) {
32139
- assertOpen();
32140
- const { rows } = await pool.query(`INSERT INTO stackbone_platform.trigger_deliveries
32141
- (id, subscription_id, provider_item_id, status)
32142
- VALUES ($1::uuid, $2::uuid, $3, 'claimed')
32143
- ON CONFLICT (subscription_id, provider_item_id) DO NOTHING
32144
- RETURNING id`, [
32145
- randomUUID(),
32146
- subscriptionId,
32147
- providerItemId
32148
- ]);
32149
- return rows.length > 0;
32150
- },
32151
- async markStarted(subscriptionId, providerItemId, runId) {
32152
- assertOpen();
32153
- await pool.query(`UPDATE stackbone_platform.trigger_deliveries
32154
- SET status = 'started', run_id = $3, updated_at = now()
32155
- WHERE subscription_id = $1::uuid AND provider_item_id = $2`, [
32156
- subscriptionId,
32157
- providerItemId,
32158
- runId
32159
- ]);
32160
- },
32161
- async markResumed(subscriptionId, providerItemId, runId) {
32162
- assertOpen();
32163
- await pool.query(`UPDATE stackbone_platform.trigger_deliveries
32164
- SET status = 'resumed', run_id = COALESCE($3, run_id), updated_at = now()
32165
- WHERE subscription_id = $1::uuid AND provider_item_id = $2`, [
32166
- subscriptionId,
32167
- providerItemId,
32168
- runId ?? null
32169
- ]);
32170
- },
32171
- async markRejected(subscriptionId, providerItemId, issues) {
32172
- assertOpen();
32173
- await pool.query(`UPDATE stackbone_platform.trigger_deliveries
32174
- SET status = 'rejected', issues = $3::jsonb, updated_at = now()
32175
- WHERE subscription_id = $1::uuid AND provider_item_id = $2`, [
32176
- subscriptionId,
32177
- providerItemId,
32178
- JSON.stringify(issues)
32179
- ]);
32180
- },
32181
- async release(subscriptionId, providerItemId) {
32182
- assertOpen();
32183
- await pool.query(`DELETE FROM stackbone_platform.trigger_deliveries
32184
- WHERE subscription_id = $1::uuid AND provider_item_id = $2`, [subscriptionId, providerItemId]);
32185
- },
32186
- close
32187
- };
32188
- };
32189
- //#endregion
32190
32766
  //#region src/services/scaffold/file-plan.ts
32191
32767
  /**
32192
32768
  * Concatenate plans into one. On a duplicate path the FIRST plan wins, so a
@@ -32299,7 +32875,7 @@ var calls_workflow_ts_default = "import { z } from \"zod\";\nimport { streamDeep
32299
32875
  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` (`z.infer`), so there is no\n// second type that drifts.\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) {\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";
32300
32876
  //#endregion
32301
32877
  //#region src/services/scaffold/template-loader.ts
32302
- var SDK_TEMPLATE_VERSION = "0.2.5";
32878
+ var SDK_TEMPLATE_VERSION = "0.2.6";
32303
32879
  var PREFIX = "./templates/";
32304
32880
  var templates = new Map(Object.entries(/* @__PURE__ */ Object.assign({
32305
32881
  "./templates/deep-agents/index.ts.tmpl": index_ts_default,
@@ -33619,7 +34195,7 @@ var runValidateContract = async (ctx, args, deps = {}) => {
33619
34195
  const versionCheck = requireVersion(contract);
33620
34196
  if (versionCheck.error) violations.push(versionCheck.error.message);
33621
34197
  for (const capability of manifest.capabilities) {
33622
- const check = requireCapability(capability, contract);
34198
+ const check = requireCapability$1(capability, contract);
33623
34199
  if (check.error) violations.push(check.error.message);
33624
34200
  }
33625
34201
  const ok = violations.length === 0;
@@ -35863,12 +36439,32 @@ async function suggest(sourceSample, targetSchema, sourceSchema) {
35863
36439
  'from the source event. Return STRICT JSON of the shape ' +
35864
36440
  '{ "suggestions": [ { "targetPath": "<dotted path in the target>", "jsonata": "<JSONata expression over the source>", "confidence": <0..1>, "note": "<short reason>" } ] }. ' +
35865
36441
  'Only include a suggestion when you are reasonably confident. Omit fields you cannot map. ' +
35866
- 'Return an empty suggestions array if nothing maps.';
35867
- const payload = { sourceSample: sourceSample, targetSchema: targetSchema };
36442
+ 'Return an empty suggestions array if nothing maps.\\n' +
36443
+ // Without this the model writes paths relative to the JSON envelope below
36444
+ // ("sourceSample.subject"), which evaluates to nothing against a real event.
36445
+ 'CRITICAL — each expression is evaluated with THE SOURCE EVENT ITSELF as the root ' +
36446
+ 'context. Write the path exactly as it appears inside the event: for an event ' +
36447
+ '{ "subject": "Hi", "from": { "address": "a@b.c" } } the expressions are "subject" and ' +
36448
+ '"from.address". NEVER prefix a path with "sourceSample", "event", "input" or any ' +
36449
+ 'other wrapper name — those are labels in the prompt below, not part of the data.\\n' +
36450
+ 'Return the BARE field reference by default. Do not change case, trim, reformat or ' +
36451
+ 'otherwise decorate a value on your own initiative: copying "subject" into a plain ' +
36452
+ 'string field is "subject", not $uppercase(subject). Transform only when the TARGET ' +
36453
+ 'field asks for it — a different type, a declared format or enum, or its own ' +
36454
+ 'description spelling out the shape it wants — and then use ONLY built-in JSONata ' +
36455
+ 'functions, which all start with "$" (e.g. $substring, $join, $number). Never invent ' +
36456
+ 'a function name.';
36457
+ const parts = [
36458
+ 'SOURCE EVENT SAMPLE (the root context your expressions run against):',
36459
+ JSON.stringify(sourceSample, null, 2),
36460
+ '',
36461
+ 'TARGET WORKFLOW INPUT SCHEMA (what targetPath addresses):',
36462
+ JSON.stringify(targetSchema, null, 2),
36463
+ ];
35868
36464
  if (sourceSchema) {
35869
- payload.sourceSchema = sourceSchema;
36465
+ parts.push('', 'SOURCE EVENT JSON SCHEMA:', JSON.stringify(sourceSchema, null, 2));
35870
36466
  }
35871
- const user = 'Map this source event to the target schema:\\n' + JSON.stringify(payload, null, 2);
36467
+ const user = parts.join('\\n');
35872
36468
 
35873
36469
  const result = await stackbone.ai.chat.completions.create({
35874
36470
  model: MAPPING_SUGGEST_MODEL,
@@ -37650,6 +38246,7 @@ var startDevSession = async (input) => {
37650
38246
  onConnectLog: (level, message) => input.ctx.logger[level === "warn" ? "warn" : "debug"]({ stage: "connect" }, message),
37651
38247
  onDeepAgentLog: (level, message) => input.ctx.logger[level]({ stage: "deep-agents" }, message),
37652
38248
  ...workflowRuntime ? { workflow: workflowRuntime } : {},
38249
+ connectRefreshRedisUrl: devRedisUrl,
37653
38250
  deepAgents: {
37654
38251
  projectRoot: input.projectRoot,
37655
38252
  agents: workspace.deepAgents ?? []
@@ -38242,7 +38839,7 @@ Available topics:
38242
38839
  cli/agent-protocol The HTTP contract a Stackbone workspace exposes: OpenAI-compatible, Anthropic-compatible and AG-UI chat, durable workflows, and health.
38243
38840
  cli/local-development How stackbone dev boots your agents and durable workflows locally, injects env vars and tunnels to cloud Studio.
38244
38841
  cli/agent-yaml How a Stackbone project is declared — workspace discovery by convention (deep-agents/<name>/index.ts + workflows/), the optional agent.yaml manifest, and the optional stackbone.config.ts override.
38245
- cli/troubleshooting Diagnose and fix the common stackbone dev and stackbone publish failures, keyed on the error codes the CLI actually emits.
38842
+ cli/troubleshooting Diagnose and fix the common stackbone dev and agent-runtime failures, keyed on the error codes the CLI actually emits.
38246
38843
 
38247
38844
  Run \`stackbone docs <topic>\` to view one.
38248
38845
  `,
@@ -38262,9 +38859,10 @@ A Stackbone project is two things working together:
38262
38859
  approvals, built on the [Workflow SDK](https://workflow-sdk.dev/docs).
38263
38860
  Steps run once, are persisted, and retry on failure.
38264
38861
 
38265
- You author both with the ambient \`stackbone\` client, run them locally with
38266
- \`stackbone dev\`, and ship them with \`stackbone publish\`. This page gets you
38267
- to a running agent on your machine.
38862
+ You author both with the ambient \`stackbone\` client and run them locally with
38863
+ \`stackbone dev\`. To go live you deploy the agent's container image into your
38864
+ own cloud and register it with \`stackbone link\`. This page gets you to a
38865
+ running agent on your machine.
38268
38866
 
38269
38867
  ## Prerequisites
38270
38868
 
@@ -38568,8 +39166,8 @@ workspace is read straight from two folders:
38568
39166
  \`<camelCase(name)>Workflow\` function (e.g. \`qualify-lead.workflow.ts\` exports
38569
39167
  \`qualifyLeadWorkflow\`).
38570
39168
 
38571
- \`stackbone dev\` and \`stackbone publish\` both read this same convention. An
38572
- optional \`stackbone.config.ts\` (default-exporting \`defineWorkspace({ workflows })\`)
39169
+ \`stackbone dev\` reads this convention. An optional \`stackbone.config.ts\`
39170
+ (default-exporting \`defineWorkspace({ workflows })\`)
38573
39171
  is an **override** for workflows only: when it exists it wins over the
38574
39172
  convention scan for workflows; agents are always discovered from
38575
39173
  \`deep-agents/\`. Most projects need no \`stackbone.config.ts\` at all.
@@ -38656,19 +39254,17 @@ control plane, the one \`stackbone init\`, \`stackbone link\` and
38656
39254
  stored per environment in \`~/.stackbone/credentials.json\`, so logging
38657
39255
  into a different control plane keeps its own selection.
38658
39256
 
38659
- \`stackbone publish\` is **not** affected by this setting: it always
38660
- publishes to the organization recorded in that project's
38661
- \`.stackbone/project.json\`. So the way to publish into a specific
38662
- organization is to switch first, then create the project there:
39257
+ An already-created project is **not** affected by this setting: it stays with
39258
+ the organization recorded in its \`.stackbone/project.json\`. So the way to work
39259
+ in a specific organization is to switch first, then create the project there:
38663
39260
 
38664
39261
  \`\`\`bash
38665
39262
  stackbone organization use # interactive picker (TTY)
38666
39263
  stackbone organization use acme # switch straight to a slug
38667
39264
 
38668
- # Publish into a chosen org:
39265
+ # Create a workspace inside a chosen org:
38669
39266
  stackbone organization use acme # 1. make "acme" active
38670
39267
  stackbone init my-app --with agent # 2. the new workspace is registered in "acme"
38671
- stackbone publish # 3. publishes to "acme" (from project.json)
38672
39268
  \`\`\`
38673
39269
 
38674
39270
  - With a \`slug\` argument it switches directly (works in CI and \`--json\`).
@@ -38723,7 +39319,7 @@ rely on it.
38723
39319
  Scaffold a new workspace. \`init\` is **workspace-first** and **links the
38724
39320
  workspace** to your organization: it registers the workspace's identity in
38725
39321
  the control plane and writes a \`.stackbone/project.json\` (so \`dev\`,
38726
- \`publish\` and the management commands know which org to talk to). You
39322
+ \`link\` and the management commands know which org to talk to). You
38727
39323
  must be signed in (\`stackbone login\`) first, there is no offline \`init\`.
38728
39324
  Alongside the link it writes the workspace shell.
38729
39325
 
@@ -38882,16 +39478,56 @@ workspace), \`1\` generic (a name collision without \`--force\`, or an unknown
38882
39478
 
38883
39479
  ### \`stackbone link\`
38884
39480
 
38885
- Link the current directory to an existing organization + agent. Writes
38886
- \`.stackbone/project.json\`, patches \`.gitignore\` and re-runs the
39481
+ Link the current directory to an existing organization + agent, and register
39482
+ the **deployment** you run that agent on. Writes \`.stackbone/project.json\`,
39483
+ patches \`.gitignore\` and re-runs the
38887
39484
  [Stackbone agent skills install](/docs/coding-agents/overview) (same
38888
39485
  best-effort step \`init\` performs). Does **not** scaffold starter files,
38889
39486
  use \`init\` for that.
38890
39487
 
38891
- | Flag | Type | Description |
38892
- | --------- | ------- | ----------------------------------------------------------------------------- |
38893
- | \`--agent\` | string | Agent slug to link to. Required in non-interactive mode (CI, \`--json\`, \`-y\`). |
38894
- | \`--force\` | boolean | Overwrite an existing \`.stackbone/project.json\`. |
39488
+ You host the agent yourself: you build and deploy the container image into
39489
+ your own cloud, then tell Stackbone where it lives. That is what the three
39490
+ required flags do. \`--url\` is the public URL the container answers on,
39491
+ \`--secret\` is the \`HMAC_SECRET\` you set on the container (it must match
39492
+ exactly), and \`--tag\` is the image tag you deployed. The tag is the agent's
39493
+ version: there is no separate build step to promote.
39494
+
39495
+ \`\`\`sh
39496
+ stackbone link \\
39497
+ --agent support \\
39498
+ --url https://support.acme.example.com \\
39499
+ --secret "$STACKBONE_HMAC_SECRET" \\
39500
+ --tag 1.4.0
39501
+ \`\`\`
39502
+
39503
+ All three flags are validated before any network call, so a missing one fails
39504
+ immediately instead of half-linking the project. Re-running \`link\` for the same
39505
+ agent updates the existing deployment (the new URL, secret and tag win) rather
39506
+ than creating a second one, so it is safe to run on every deploy.
39507
+
39508
+ | Flag | Type | Description |
39509
+ | ---------- | ------- | ------------------------------------------------------------------------------------------------ |
39510
+ | \`--agent\` | string | Agent slug to link to. Required in non-interactive mode (CI, \`--json\`, \`-y\`). |
39511
+ | \`--url\` | string | **Required.** Public URL where the deployed agent is reachable. |
39512
+ | \`--secret\` | string | **Required.** The \`HMAC_SECRET\` you set on the deployed container. Must match it exactly. |
39513
+ | \`--tag\` | string | **Required.** Tag of the image you deployed (a semver or a digest). This is the agent's version. |
39514
+ | \`--force\` | boolean | Overwrite an existing \`.stackbone/project.json\`. |
39515
+
39516
+ **The deploy then link flow.** You generate an HMAC secret, set it as the
39517
+ \`HMAC_SECRET\` env var on the container, start the container, then run
39518
+ \`stackbone link --agent <slug> --url <url> --secret <value> --tag <tag>\`. The control plane signs
39519
+ every request it later proxies to your box with this secret, and your box
39520
+ verifies the signature, so the two values have to be identical.
39521
+
39522
+ **\`link\` proves the secret before it saves anything.** Before it registers the
39523
+ deployment, \`link\` signs a challenge request to your container and your container
39524
+ verifies it against its own \`HMAC_SECRET\`. A match saves the secret (encrypted at
39525
+ rest). A mismatch stops with an actionable error and saves nothing, so a typo
39526
+ fails at \`link\` time instead of quietly breaking the proxy later. If your box is
39527
+ unreachable, \`link\` says so and saves nothing.
39528
+
39529
+ If you registered a deployment before this check existed, re-run \`stackbone link\`
39530
+ once to validate and re-save the secret.
38895
39531
 
38896
39532
  **JSON payload**
38897
39533
 
@@ -38901,6 +39537,13 @@ use \`init\` for that.
38901
39537
  "agent": { "id": "...", "slug": "...", "name": "..." },
38902
39538
  "organization_id": "...",
38903
39539
  "local_dev_installation": { "id": "...", "organization_slug": "..." },
39540
+ "deployment": {
39541
+ "id": "...",
39542
+ "url": "https://support.acme.example.com",
39543
+ "image_tag": "1.4.0",
39544
+ "status": "...",
39545
+ "last_active_at": "2026-07-21T09:00:00Z" /* or null before the first call */,
39546
+ },
38904
39547
  "target_dir": "/abs/path",
38905
39548
  "files_written": [".stackbone/project.json", ".gitignore"],
38906
39549
  "skills_install": {
@@ -39307,61 +39950,13 @@ Classify each migration as \`applied\`, \`pending\` or \`drifted\`
39307
39950
  **Exit codes**: \`0\` ok (regardless of drift), \`3\` no project, \`1\`
39308
39951
  generic (cannot reach the DB).
39309
39952
 
39310
- ## Cloud publish
39311
-
39312
- ### \`stackbone publish\`
39313
-
39314
- Package the project into a publishable artifact. \`publish\` reads the
39315
- **workspace** by convention, every \`deep-agents/<name>/index.ts\` (or an
39316
- explicit \`stackbone.config.ts\` override for workflows), and compiles
39317
- every agent and every durable workflow into a verifiable tar at
39318
- \`dist/stackbone/workspace-bundle.tar\`.
39319
-
39320
- Native dependencies (\`.node\` add-ons) are rejected up front, the runtime image
39321
- is pure-JS, so a native bundle would crash at boot.
39322
-
39323
- | Flag | Type | Default | Description |
39324
- | -------- | ------- | ------- | -------------------------------------------------------- |
39325
- | \`--json\` | boolean | \`false\` | Emit structured JSON envelopes instead of the pretty UI. |
39326
-
39327
- Run \`stackbone publish --help\` for the full reference.
39328
-
39329
- #### Workspace (discovered by convention)
39330
-
39331
- Compiles every agent and every durable workflow **on your machine** and packs
39332
- them into a tar so cold boot only extracts, it never recompiles. In order:
39333
-
39334
- 1. Load the workspace and its agents + workflows.
39335
- 2. **Native-deps gate, per agent**: a \`.node\` add-on aborts with the offending
39336
- package name.
39337
- 3. Compile each agent to a prebuilt ESM module, keeping the agent-authoring
39338
- library and its model provider packages external so one copy of each
39339
- resolves from the runtime image at boot.
39340
- 4. **SDK-external guardrail, per agent**: an agent that inlined \`@stackbone/sdk\`
39341
- aborts (a second copy of the SDK would break per-run logging).
39342
- 5. Compile the workflows.
39343
- 6. Derive the \`.well-known/workspace.json\` manifest, pack the tar and compute its
39344
- SHA-256 digest.
39953
+ ## Going to production
39345
39954
 
39346
- The artifacts land in your project under \`dist/stackbone/\`:
39347
- \`workspace-bundle.tar\` (the bundle) and \`workspace-bundle.json\` (a pointer with
39348
- the digest, size, and the agent + workflow names). The control-plane upload is
39349
- handled by provisioning when you ship the install, \`publish\` produces and
39350
- verifies the bundle.
39351
-
39352
- **JSON payload (workspace)**
39353
-
39354
- \`\`\`jsonc
39355
- {
39356
- "schema_version": 1,
39357
- "kind": "workspace",
39358
- "digest": "<sha256>",
39359
- "sizeBytes": 1234567,
39360
- "agents": ["support", "billing"],
39361
- "workflows": ["onboarding"],
39362
- "tar": "dist/stackbone/workspace-bundle.tar",
39363
- }
39364
- \`\`\`
39955
+ There is no publish command. You run the agent in **your own cloud**: build
39956
+ the container image, deploy it wherever you host services, then register it
39957
+ with [\`stackbone link\`](#stackbone-link) so Stackbone knows its URL, its HMAC
39958
+ secret and the image tag it is running. The tag is the agent's version, so
39959
+ shipping a new version means deploying a new image and re-running \`link\`.
39365
39960
 
39366
39961
  ## Inline help
39367
39962
 
@@ -39464,15 +40059,14 @@ Lives under \`~/.stackbone/\`:
39464
40059
 
39465
40060
  Lives at the project root:
39466
40061
 
39467
- | Path | Tracked? | What it stores |
39468
- | --------------------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
39469
- | \`stackbone.config.ts\` | **Optional** | An _optional_ workflow override. By default \`stackbone dev\` and \`stackbone publish\` **derive** the workspace registry by convention — scanning every \`deep-agents/<name>/index.ts\` and every \`workflows/<name>.workflow.ts\` file — so most projects need no config file at all. When this file is present (default-exporting \`defineWorkspace({ workflows })\` from \`@stackbone/sdk\`) its \`workflows\` **win** over the convention scan; agents are always discovered from \`deep-agents/\`. See [Workflows](/docs/workflows/overview). |
39470
- | \`.stackbone/project.json\` | **No** (gitignored by \`stackbone init\`) | \`{ schemaVersion, organizationId, agentId, controlPlaneUrl }\`. Identifies which organization this workspace points at. |
39471
- | \`.stackbone/*.d.ts\` | **No** (gitignored) | Generated editor types — \`config.d.ts\`, \`agents.d.ts\`, \`connect.d.ts\` — that give the ambient \`stackbone\` client typed autocompletion. Regenerated by \`stackbone dev\`; \`config.d.ts\` also by \`stackbone config types\` (no control plane needed). |
39472
- | \`stackbone.config.json\` | **Yes** | \`{ studio: { corsOrigins } }\` and other team-shared knobs. Reviewable in PRs. **Different file** from \`stackbone.config.ts\` above — the \`.json\` holds CLI/Studio knobs, the \`.ts\` holds the workspace manifest. |
39473
- | \`agent.yaml\` | **Yes** | Manifest. See [agent.yaml reference](/docs/cli/agent-yaml). |
39474
- | \`dist/stackbone/\` | **No** (build output) | Written by \`stackbone publish\` — the workspace bundle (\`workspace-bundle.tar\`) plus its JSON pointer (\`workspace-bundle.json\`, carrying the content digest). |
39475
- | \`.claude/\`, \`.cursor/\`, \`.windsurf/\`, … | **No** (gitignored by \`stackbone init\` / \`link\`) | Per-coding-agent skill directories the upstream \`skills\` registry materialises. See [Agent skills overview](/docs/coding-agents/overview). |
40062
+ | Path | Tracked? | What it stores |
40063
+ | --------------------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
40064
+ | \`stackbone.config.ts\` | **Optional** | An _optional_ workflow override. By default \`stackbone dev\` **derives** the workspace registry by convention (scanning every \`deep-agents/<name>/index.ts\` and every \`workflows/<name>.workflow.ts\` file), so most projects need no config file at all. When this file is present (default-exporting \`defineWorkspace({ workflows })\` from \`@stackbone/sdk\`) its \`workflows\` **win** over the convention scan; agents are always discovered from \`deep-agents/\`. See [Workflows](/docs/workflows/overview). |
40065
+ | \`.stackbone/project.json\` | **No** (gitignored by \`stackbone init\`) | \`{ schemaVersion, organizationId, agentId, controlPlaneUrl }\`. Identifies which organization this workspace points at. |
40066
+ | \`.stackbone/*.d.ts\` | **No** (gitignored) | Generated editor types — \`config.d.ts\`, \`agents.d.ts\`, \`connect.d.ts\` — that give the ambient \`stackbone\` client typed autocompletion. Regenerated by \`stackbone dev\`; \`config.d.ts\` also by \`stackbone config types\` (no control plane needed). |
40067
+ | \`stackbone.config.json\` | **Yes** | \`{ studio: { corsOrigins } }\` and other team-shared knobs. Reviewable in PRs. **Different file** from \`stackbone.config.ts\` above — the \`.json\` holds CLI/Studio knobs, the \`.ts\` holds the workspace manifest. |
40068
+ | \`agent.yaml\` | **Yes** | Manifest. See [agent.yaml reference](/docs/cli/agent-yaml). |
40069
+ | \`.claude/\`, \`.cursor/\`, \`.windsurf/\`, … | **No** (gitignored by \`stackbone init\` / \`link\`) | Per-coding-agent skill directories the upstream \`skills\` registry materialises. See [Agent skills overview](/docs/coding-agents/overview). |
39476
40070
 
39477
40071
  ## Output contract
39478
40072
 
@@ -40144,8 +40738,7 @@ them down in reverse order:
40144
40738
  ### How the workspace is discovered
40145
40739
 
40146
40740
  A **workspace** is discovered by convention from the files on disk, you do
40147
- not maintain a registry. \`stackbone dev\` (and \`stackbone publish\`) scan your
40148
- project the same way:
40741
+ not maintain a registry. \`stackbone dev\` scans your project this way:
40149
40742
 
40150
40743
  - **Agents**: every folder under \`deep-agents/\` that contains an \`index.ts\`
40151
40744
  exporting an agent definition. The folder name is both the agent name and
@@ -40421,8 +41014,8 @@ the protocol version. If your CLI is older than \`minSupported\` the UI shows a
40421
41014
  > in only when you need to name your workflows explicitly, and it wins over
40422
41015
  > the convention scan for workflows.
40423
41016
  >
40424
- > \`stackbone dev\` and \`stackbone publish\` both read the same convention: they
40425
- > discover the workspace from every \`deep-agents/<name>/index.ts\` on disk
41017
+ > \`stackbone dev\` reads the convention: it
41018
+ > discovers the workspace from every \`deep-agents/<name>/index.ts\` on disk
40426
41019
  > (agents are always discovered this way; \`stackbone.config.ts\` can only
40427
41020
  > override the workflow list). When present, \`agent.yaml\` is validated
40428
41021
  > strictly and belongs under version control.
@@ -40430,8 +41023,8 @@ the protocol version. If your CLI is older than \`minSupported\` the UI shows a
40430
41023
  ## Workspace discovery by convention
40431
41024
 
40432
41025
  The runtime derives the workspace from the files on disk, there is no
40433
- hand-maintained registry. \`stackbone dev\` and \`stackbone publish\` both read the
40434
- same two conventions:
41026
+ hand-maintained registry. \`stackbone dev\` reads the same two conventions the
41027
+ runtime does:
40435
41028
 
40436
41029
  - **Agents**: every folder under \`deep-agents/\` that contains an \`index.ts\`
40437
41030
  entry file. The folder name is both the agent's identity and the \`model\` a
@@ -40512,17 +41105,6 @@ your editor. They are derived artifacts, git-ignore them:
40512
41105
  | \`.stackbone/connect.d.ts\` | \`stackbone.connection('<id>')\` and its connector methods. |
40513
41106
  | \`.stackbone/config.d.ts\` | \`stackbone.config.get('<key>')\` typed from your config schema. |
40514
41107
 
40515
- ### What \`stackbone publish\` produces
40516
-
40517
- For a workspace, \`stackbone publish\` compiles every agent and every workflow on
40518
- your machine and packs them into a single tar at
40519
- \`dist/stackbone/workspace-bundle.tar\`, next to a \`workspace-bundle.json\` pointer
40520
- (digest, size, the agent + workflow names). Inside the tar is a
40521
- \`.well-known/workspace.json\` manifest the runtime reads at boot to know which
40522
- agents and workflows to launch. The published bundle is pure JavaScript, native
40523
- add-ons are rejected up front, so publish fails early if a dependency is not
40524
- pure-JS.
40525
-
40526
41108
  ## \`agent.yaml\`: the optional workspace manifest
40527
41109
 
40528
41110
  A workspace may carry a single \`agent.yaml\` at its root describing project-wide
@@ -40554,9 +41136,11 @@ Human-readable workspace name. Must be a non-empty string.
40554
41136
 
40555
41137
  ### \`version\` (optional)
40556
41138
 
40557
- A display-only label copied verbatim onto the build at publish time (e.g.
40558
- \`v0.2.0\`). Never validated as semver, never made unique, it just renders next to
40559
- the build on the install detail page. Omit it and the build carries no label.
41139
+ A display-only label (e.g. \`v0.2.0\`). Never validated as semver, never made
41140
+ unique, it just renders on the install detail page. The version that actually
41141
+ identifies what is running is the image tag you register with
41142
+ \`stackbone link --tag\`, see
41143
+ [Commands → \`stackbone link\`](/docs/cli/commands#stackbone-link).
40560
41144
 
40561
41145
  \`\`\`yaml
40562
41146
  version: v0.2.0
@@ -40711,11 +41295,10 @@ on disk, plus every \`workflows/<name>.workflow.ts\` (or an explicit
40711
41295
  \`stackbone.config.ts\` override for workflows). The optional root \`agent.yaml\`
40712
41296
  supplies the project-wide database/runtime knobs when it is present.
40713
41297
 
40714
- | Command | What it reads |
40715
- | ------------------- | ----------------------------------------------------------------- |
40716
- | \`stackbone dev\` | The discovered workspace, boots every agent + every workflow. |
40717
- | \`stackbone publish\` | The discovered workspace → \`dist/stackbone/workspace-bundle.tar\`. |
40718
- | \`stackbone db ...\` | The root \`agent.yaml\` \`database\` block, when present. |
41298
+ | Command | What it reads |
41299
+ | ------------------ | ------------------------------------------------------------- |
41300
+ | \`stackbone dev\` | The discovered workspace, boots every agent + every workflow. |
41301
+ | \`stackbone db ...\` | The root \`agent.yaml\` \`database\` block, when present. |
40719
41302
 
40720
41303
  Every manifest is yours to edit and belongs under version control, the control
40721
41304
  plane never writes to them.
@@ -40885,68 +41468,22 @@ schema\`) is exercised through \`stackbone dev\` today. Pointed at an installati
40885
41468
  that does not serve the workflow routes, those commands come back empty rather
40886
41469
  than erroring.
40887
41470
 
40888
- ## \`stackbone publish\` failures
40889
-
40890
- \`stackbone publish\` compiles your whole workspace **once**, on your machine,
40891
- into a single bundle. Every guardrail below aborts _before_ a bundle is
40892
- produced — a failed publish ships **nothing**, which is the point: a publish
40893
- error is always better than a crash-loop in production. Each abort prints the
40894
- exact fix; here is what each one means.
40895
-
40896
- ### \`@stackbone/sdk\` inlined into an agent build
40897
-
40898
- > \`stackbone publish failed: deep agent "<name>" inlined @stackbone/sdk into its build.\`
40899
-
40900
- A workspace agent must keep \`@stackbone/sdk\` **external**, so it resolves to a
40901
- single copy at runtime. A second copy gives the agent its own ambient context
40902
- and per-run logs arrive empty. \`defineDeepAgent\` from \`@stackbone/sdk/deep\`
40903
- keeps the SDK external for you automatically; this error only appears if
40904
- something in your build pipeline forces it to inline anyway (for example a
40905
- bundler config outside \`stackbone publish\` re-processing the output). Remove
40906
- whatever is re-bundling \`@stackbone/sdk\` and re-publish.
40907
-
40908
- ### Native dependencies rejected
40909
-
40910
- > \`stackbone publish failed: native dependencies detected in deep agent "<name>".\`
40911
-
40912
- The workspace runtime runs pure-JS agents only. A dependency with native
40913
- bindings (\`.node\` files) is rejected with the offending package name. Remove the
40914
- native dependency from that agent.
40915
-
40916
- ### No agents declared
40917
-
40918
- > \`This workspace declares no deep agents (none found under deep-agents/*/index.ts) — a workspace bundle needs at least one deep agent.\`
40919
-
40920
- The workspace has no agents, there is no \`deep-agents/<name>/index.ts\` to
40921
- discover. A workspace bundle needs at least one, so add one (for example
40922
- \`stackbone add agent <name>\`, which scaffolds \`deep-agents/<name>/\`) and
40923
- re-publish.
40924
-
40925
- ### An agent's build failed
40926
-
40927
- > \`esbuild failed for deep agent "<name>": …\`
40928
-
40929
- One agent's \`index.ts\` failed to bundle. The message carries the underlying
40930
- esbuild error (a syntax error, a missing import); fix it and re-publish.
40931
-
40932
- ### Incompatible \`@stackbone/sdk\`
41471
+ ## Incompatible \`@stackbone/sdk\`
40933
41472
 
40934
41473
  > \`@stackbone/sdk@<version> is not compatible with this CLI, which supports >=0.1.0 <0.2.0 (built against 0.1.0).\`
40935
41474
 
40936
- Each CLI build supports one minor line of \`@stackbone/sdk\`. \`stackbone publish\`
40937
- blocks when the version installed in your project falls outside that line,
40938
- because an incompatible SDK is what would ship to cloud. Pin the SDK to a
41475
+ Each CLI build supports one minor line of \`@stackbone/sdk\`. \`stackbone dev\`
41476
+ checks the version installed in your project on boot and warns when it falls
41477
+ outside that line, then keeps the local loop running. Pin the SDK to a
40939
41478
  supported version and reinstall:
40940
41479
 
40941
41480
  \`\`\`sh
40942
41481
  pnpm add -E @stackbone/sdk@<version>
40943
41482
  \`\`\`
40944
41483
 
40945
- The same check runs at the start of \`stackbone dev\`, but there it only warns and
40946
- keeps the local loop running. If the SDK is not installed at all, the message
40947
- tells you to run \`pnpm install\` first instead. A scaffolded project pins the SDK
40948
- exactly (no caret), so a stray \`pnpm update\` cannot silently move it off the
40949
- supported line.
41484
+ If the SDK is not installed at all, the message tells you to run \`pnpm install\`
41485
+ first instead. A scaffolded project pins the SDK exactly (no caret), so a stray
41486
+ \`pnpm update\` cannot silently move it off the supported line.
40950
41487
 
40951
41488
  ## Checking a deployed workspace's health
40952
41489
 
@@ -41173,7 +41710,7 @@ engine behind [Vercel Workflows](https://vercel.com/docs/workflows)), so a
41173
41710
  run survives crashes and redeploys and can pause for months.
41174
41711
 
41175
41712
  You don't register workflows by hand. The workspace is **discovered by
41176
- convention**: \`stackbone dev\` and \`stackbone publish\` scan your project and
41713
+ convention**: \`stackbone dev\` and the deployed runtime scan your project and
41177
41714
  pick up every agent folder under \`deep-agents/\` that has an \`index.ts\`, and
41178
41715
  every workflow file at \`workflows/<name>.workflow.ts\`. A workflow's name is
41179
41716
  the file basename without the \`.workflow.ts\` suffix, and its exported
@@ -41468,7 +42005,7 @@ suppressed.** Production agents should leave this flag unset.
41468
42005
  - **[Logging & observability](/docs/sdk/observability)** — the run timeline is
41469
42006
  captured for you.
41470
42007
  - **[CLI reference](/docs/cli/commands)** — \`stackbone dev\`,
41471
- \`stackbone publish\`, \`stackbone workflows\`, \`stackbone runs\`,
42008
+ \`stackbone link\`, \`stackbone workflows\`, \`stackbone runs\`,
41472
42009
  \`stackbone hitl\`.
41473
42010
  `,
41474
42011
  "sdk/ai": `# stackbone.ai
@@ -42929,9 +43466,10 @@ subpath imports it directly.
42929
43466
  - **Records the approval** so the run shows up in the inbox. The row is
42930
43467
  tied to its run, so you can find it from the run in the dashboard or
42931
43468
  with \`stackbone hitl list\`.
42932
- - **Pauses the run** on a hook keyed by your \`token\`. The pause is
42933
- durable — the hook's state lives in Redis, so the run survives a
42934
- crash, a cold start, or a redeploy and resumes deterministically.
43469
+ - **Pauses the run** on a hook with its own resume key. The SDK assigns
43470
+ that key per run, so you don't invent one. The pause is durable: the
43471
+ hook's state lives in Redis, so the run survives a crash, a cold
43472
+ start, or a redeploy and resumes deterministically.
42935
43473
  - **Races the decision against a timeout.** If a human decides first,
42936
43474
  the run resumes with that decision. If the \`timeout\` elapses first,
42937
43475
  the \`fallback\` (\`'approve'\` or \`'reject'\`) is applied instead.
@@ -42955,7 +43493,6 @@ import { requestApproval } from '@stackbone/sdk/workflow';
42955
43493
  export const inputSchema = z.object({
42956
43494
  orderId: z.string(),
42957
43495
  amount: z.number().positive(),
42958
- approvalToken: z.string(),
42959
43496
  });
42960
43497
 
42961
43498
  export const outputSchema = z.object({
@@ -42968,7 +43505,7 @@ export async function refundWorkflow(input: z.infer<typeof inputSchema>) {
42968
43505
  'use workflow';
42969
43506
 
42970
43507
  const decision = await requestApproval({
42971
- token: input.approvalToken,
43508
+ // No \`token\`: the SDK assigns a unique resume key for this run.
42972
43509
  topic: 'refund',
42973
43510
  payload: { orderId: input.orderId, amount: input.amount },
42974
43511
  title: 'Approve this refund?',
@@ -43002,12 +43539,12 @@ crash, a redeploy — is handled by the durable pause.
43002
43539
 
43003
43540
  | Field | Type | Notes |
43004
43541
  | ---------- | ----------------------- | ----------------------------------------------------------------------------------------- |
43005
- | \`token\` | \`string\` | **Required.** The resume key — stable and unguessable, unique per approval within a run. |
43006
43542
  | \`topic\` | \`string\` | **Required.** Approval category, shown in the inbox (e.g. \`'refund'\`, \`'deploy'\`). |
43007
43543
  | \`payload\` | \`T\` | **Required.** What the reviewer looks at when deciding. |
43008
43544
  | \`title\` | \`string\` | Optional. Human-readable title surfaced in the inbox and run view. |
43009
43545
  | \`timeout\` | \`string \\| number\` | Optional. ISO 8601 duration (\`'24h'\`, \`'15m'\`) or milliseconds. Races the human decision. |
43010
43546
  | \`fallback\` | \`'approve' \\| 'reject'\` | Optional. Applied when the timeout wins the race. Defaults to \`'reject'\`. |
43547
+ | \`token\` | \`string\` | Optional. Your own resume key. Omit it: the SDK assigns a unique one per run. |
43011
43548
 
43012
43549
  It returns an \`ApprovalDecision\`:
43013
43550
 
@@ -43023,6 +43560,21 @@ Branch on \`status\`. Use \`timedOut\` if you want to treat a fallback
43023
43560
  differently from a real human decision (for example, to alert that
43024
43561
  nobody responded in time).
43025
43562
 
43563
+ ### Should you pass your own \`token\`?
43564
+
43565
+ Almost never. Leave \`token\` out and the SDK generates a resume key that
43566
+ is unique to that run, so a retry, or a second run started while the
43567
+ first approval is still pending, can't collide with a key another live
43568
+ run already holds. Two gates in the same run each get their own key too.
43569
+
43570
+ Pass an explicit \`token\` only when something outside the run has to
43571
+ rebuild the same key to resume it, and then make sure it is unique per
43572
+ approval per run. Reusing a key another active run still holds is the
43573
+ one case that can't resume: the approval falls back instead of waiting
43574
+ for a person. If what you actually want is for an inbound reply to wake
43575
+ this exact run, use a raw hook rather than \`requestApproval()\`. See
43576
+ [Wait for a reply](#wait-for-a-reply).
43577
+
43026
43578
  ## Deciding
43027
43579
 
43028
43580
  When the workflow pauses, the approval lands in the inbox. A reviewer
@@ -43063,10 +43615,10 @@ Common patterns:
43063
43615
 
43064
43616
  \`\`\`ts
43065
43617
  // High-risk: auto-reject if no one looks within an hour.
43066
- await requestApproval({ token, topic, payload, timeout: '1h', fallback: 'reject' });
43618
+ await requestApproval({ topic, payload, timeout: '1h', fallback: 'reject' });
43067
43619
 
43068
43620
  // Low-risk: auto-approve after a grace period if no one objects.
43069
- await requestApproval({ token, topic, payload, timeout: '15m', fallback: 'approve' });
43621
+ await requestApproval({ topic, payload, timeout: '15m', fallback: 'approve' });
43070
43622
  \`\`\`
43071
43623
 
43072
43624
  ## Escape hatch: custom hooks
@@ -43092,10 +43644,11 @@ docs for hook and replay semantics.
43092
43644
 
43093
43645
  A common shape is not approve/reject at all. You send someone a message,
43094
43646
  and you want the **same run** to wake up when they reply. \`requestApproval()\`
43095
- does not fit here, because it models a decision and returns
43096
- \`{ status, payload, timedOut }\`. A reply is free-form content, so drop down to
43097
- a raw hook. A resumed hook returns whatever payload woke it, which is the
43098
- reply itself.
43647
+ does not fit here, for two reasons: it models a decision and returns
43648
+ \`{ status, payload, timedOut }\`, and its resume key is auto-generated, so no
43649
+ incoming event can be matched against it. A reply is free-form content and
43650
+ needs a key you choose, so drop down to a raw hook. A resumed hook returns
43651
+ whatever payload woke it, which is the reply itself.
43099
43652
 
43100
43653
  Park the hook on a \`token\` you control, and make that token the
43101
43654
  conversation's own id: the email thread id the provider hands back when you
@@ -43731,8 +44284,8 @@ the resume path.
43731
44284
  ## The workspace registry
43732
44285
 
43733
44286
  The CLI and runtime discover your workspace **by convention**, from the
43734
- files on disk, with no registry to hand-maintain. \`stackbone dev\` and
43735
- \`stackbone publish\` both read the same convention:
44287
+ files on disk, with no registry to hand-maintain. \`stackbone dev\` and the
44288
+ deployed runtime both read the same convention:
43736
44289
 
43737
44290
  - **Agents**: every folder under \`deep-agents/\` with an \`index.ts\` entry
43738
44291
  file. The folder name is both the agent's identity and the \`model\` a
@@ -43768,9 +44321,9 @@ export default defineWorkspace({
43768
44321
  });
43769
44322
  \`\`\`
43770
44323
 
43771
- When present, this file is what \`stackbone dev\`, \`stackbone publish\`,
43772
- \`stackbone workflows list\`, and the typed autocompletion below read
43773
- instead of scanning the filesystem for workflows.
44324
+ When present, this file is what \`stackbone dev\`, \`stackbone workflows list\`,
44325
+ and the typed autocompletion below read instead of scanning the filesystem for
44326
+ workflows.
43774
44327
 
43775
44328
  ## How the CLI connects to the SDK
43776
44329
 
@@ -44351,8 +44904,8 @@ Like \`stackbone.secrets\` and \`stackbone.config\`, this surface is
44351
44904
  directly, so it is not gated on the contract handshake. The only
44352
44905
  prerequisite is that the prompts schema has been applied. Those tables
44353
44906
  are created when your project's migrations run — which happens
44354
- automatically when you boot your project with \`stackbone dev\` (and again
44355
- when you \`stackbone publish\`). A not-yet-migrated agent surfaces
44907
+ automatically when you boot your project with \`stackbone dev\`, and again
44908
+ when the deployed runtime starts. A not-yet-migrated agent surfaces
44356
44909
  \`prompts_not_configured\` until then.
44357
44910
 
44358
44911
  ## API
@@ -46098,10 +46651,51 @@ var createInitCommand = (ctx) => defineCommand({
46098
46651
  });
46099
46652
  //#endregion
46100
46653
  //#region src/services/deployments.service.ts
46654
+ init_src$11();
46101
46655
  init_src$13();
46102
46656
  var registerDeployment = async (ctx, body) => {
46103
46657
  return parseResponse$2(deploymentResponseSchema, await ctx.http.post("/api/v1/deployments", body), "POST /api/v1/deployments");
46104
46658
  };
46659
+ var verifyDeploymentSecret = async (args) => {
46660
+ const fetchImpl = args.fetchImpl ?? fetch;
46661
+ let challengeUrl;
46662
+ try {
46663
+ challengeUrl = new URL(PROXY_CHALLENGE_PATH, args.url).toString();
46664
+ } catch {
46665
+ throw new StackboneCliError({
46666
+ code: "generic",
46667
+ message: `--url is not a valid URL: ${args.url}`,
46668
+ suggestion: "Pass the full public URL of the deployed agent, e.g. https://agent.example.com"
46669
+ });
46670
+ }
46671
+ const header = buildStackboneSignatureHeader({
46672
+ secrets: [args.secret],
46673
+ payload: proxyRequestPayload({
46674
+ method: "GET",
46675
+ path: PROXY_CHALLENGE_PATH,
46676
+ body: ""
46677
+ })
46678
+ });
46679
+ let response;
46680
+ try {
46681
+ response = await fetchImpl(challengeUrl, {
46682
+ method: "GET",
46683
+ headers: { "stackbone-signature": header }
46684
+ });
46685
+ } catch (cause) {
46686
+ throw new StackboneCliError({
46687
+ code: "generic",
46688
+ message: `Could not reach the deployed container at ${args.url} to verify --secret`,
46689
+ suggestion: "Check the --url is the public address of the running container and that it is reachable.",
46690
+ cause
46691
+ });
46692
+ }
46693
+ if (!response.ok) throw new StackboneCliError({
46694
+ code: "generic",
46695
+ message: `The --secret does not match the HMAC_SECRET configured on the deployed container at ${args.url}`,
46696
+ suggestion: "Set the same value the container was started with (its HMAC_SECRET env), then re-run `stackbone link`."
46697
+ });
46698
+ };
46105
46699
  //#endregion
46106
46700
  //#region src/commands/link/link.command.ts
46107
46701
  var requireLinkField = (value, flag, description) => {
@@ -46168,6 +46762,11 @@ var runLink = async (ctx, args) => {
46168
46762
  const activeOrg = await getActiveOrganization(ctx);
46169
46763
  const agent = await getAgentByOwnerAndSlug(ctx, activeOrg.slug, chosenSlug);
46170
46764
  if (!ctx.flags.json) log.step(`Linking ${targetDir} to ${agent.slug}`);
46765
+ if (!ctx.flags.json) log.step("Verifying the --secret against the deployed container");
46766
+ await verifyDeploymentSecret({
46767
+ url,
46768
+ secret
46769
+ });
46171
46770
  const [localDev, deployment] = await Promise.all([upsertLocalDevInstallation(ctx, {
46172
46771
  creatorOrganizationSlug: activeOrg.slug,
46173
46772
  agentTemplateSlug: agent.slug