@stackbone/cli 0.3.2 → 0.3.3

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
@@ -11,13 +11,13 @@ import pino from "pino";
11
11
  import pretty from "pino-pretty";
12
12
  import { exec, execFile, spawn, spawnSync } from "node:child_process";
13
13
  import { fileURLToPath, pathToFileURL } from "node:url";
14
+ import { DeleteObjectCommand, GetObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
14
15
  import sodium from "libsodium-wrappers";
15
16
  import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
16
17
  import { drizzle } from "drizzle-orm/postgres-js";
17
18
  import postgres from "postgres";
18
19
  import { extractText } from "unpdf";
19
20
  import OpenAI from "openai";
20
- import { DeleteObjectCommand, GetObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
21
21
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
22
22
  import { format, promisify } from "node:util";
23
23
  import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
@@ -153,7 +153,7 @@ var init_installations = __esmMin((() => {
153
153
  organizationSlug: organizationSlugSchema,
154
154
  agentSlug: z.string().min(1).max(80)
155
155
  });
156
- })), agentTemplateStatusSchema, agentTemplateResponseSchema, agentInstallationResponseSchema, DEFAULT_RAG_EMBEDDING_MODEL, agentManifestRagSchema, agentManifestProtocolSchema, runtimeSchema, agentManifestConnectionsSchema, recipeTriggerRefSchema, recipeActionRefSchema, agentManifestRecipeSchema, agentManifestAutomationsSchema, DEFAULT_AGENT_SCHEMA_PATH, DEFAULT_AGENT_MIGRATIONS_PATH, BUNDLE_MIGRATIONS_PATH, WORKSPACE_MANIFEST_PATH, WORKFLOW_WELL_KNOWN_DIR, WORKFLOW_STEP_FILE, WORKFLOW_VM_FILE, WORKFLOW_MANIFEST_FILE, WORKFLOW_ARTIFACTS, WORKFLOW_SCHEMAS_FILE, MANIFEST_ENTRY_KINDS, MANIFEST_ENTRY_MODES, MIN_IMAGE_VERSION_FOR_MANIFEST_V2, workflowStartSurface, workflowChatSurface, DEEP_AGENT_CHAT_SURFACE, agentManifestSchema;
156
+ })), agentTemplateStatusSchema, agentTemplateResponseSchema, agentInstallationResponseSchema, DEFAULT_RAG_EMBEDDING_MODEL, agentManifestRagSchema, agentManifestProtocolSchema, runtimeSchema, agentManifestConnectionsSchema, recipeTriggerRefSchema, recipeActionRefSchema, agentManifestRecipeSchema, agentManifestAutomationsSchema, DEFAULT_AGENT_SCHEMA_PATH, DEFAULT_AGENT_MIGRATIONS_PATH, BUNDLE_MIGRATIONS_PATH, WORKSPACE_MANIFEST_PATH, CONFIG_SCHEMA_ARTIFACT_PATH, WORKFLOW_WELL_KNOWN_DIR, WORKFLOW_STEP_FILE, WORKFLOW_VM_FILE, WORKFLOW_MANIFEST_FILE, WORKFLOW_ARTIFACTS, WORKFLOW_SCHEMAS_FILE, MANIFEST_ENTRY_KINDS, MANIFEST_ENTRY_MODES, MIN_IMAGE_VERSION_FOR_MANIFEST_V2, workflowStartSurface, workflowChatSurface, DEEP_AGENT_CHAT_SURFACE, agentManifestSchema;
157
157
  var init_agents = __esmMin((() => {
158
158
  init_json_object();
159
159
  init_connectors();
@@ -270,6 +270,7 @@ var init_agents = __esmMin((() => {
270
270
  DEFAULT_AGENT_MIGRATIONS_PATH = "./.stackbone/migrations";
271
271
  BUNDLE_MIGRATIONS_PATH = ".well-known/migrations/v1";
272
272
  WORKSPACE_MANIFEST_PATH = ".well-known/workspace.json";
273
+ CONFIG_SCHEMA_ARTIFACT_PATH = ".well-known/config/v1/config-schema.json";
273
274
  WORKFLOW_WELL_KNOWN_DIR = ".well-known/workflow/v1";
274
275
  WORKFLOW_STEP_FILE = "step.mjs";
275
276
  WORKFLOW_VM_FILE = "workflow.vm.js";
@@ -3345,6 +3346,47 @@ var bootWorkspaceRuntime = async (opts, deps = {}) => {
3345
3346
  };
3346
3347
  };
3347
3348
  //#endregion
3349
+ //#region ../../libs/shared/s3-client/src/lib/create-s3-compat-client.ts
3350
+ /**
3351
+ * The single S3-compatible client factory. Behind this seam it resolves the
3352
+ * addressing style (path-style unless the caller says otherwise), falls back to
3353
+ * `'us-east-1'` when the caller pins no region, and shapes the flat
3354
+ * `accessKeyId` / `secretAccessKey` pair into the SDK's `credentials` object.
3355
+ *
3356
+ * Call sites pin their region explicitly: studio and the SDK pass the one they
3357
+ * resolved, the emulator pins `'us-east-1'`.
3358
+ */
3359
+ /**
3360
+ * Read the addressing style out of an environment value.
3361
+ *
3362
+ * Lives here, next to the factory, so the SDK and the runtime cannot drift on
3363
+ * what counts as "off". Anything unset, empty or unrecognised means path-style
3364
+ * — the safe default, since a typo'd value silently flipping a working box to
3365
+ * subdomain addressing would break every call with an error that names neither
3366
+ * the variable nor the typo.
3367
+ */
3368
+ function parseForcePathStyle(raw) {
3369
+ const value = raw?.trim().toLowerCase();
3370
+ return !(value === "false" || value === "0" || value === "no" || value === "off");
3371
+ }
3372
+ function createS3CompatClient(options) {
3373
+ return new S3Client({
3374
+ endpoint: options.endpoint,
3375
+ region: options.region ?? "us-east-1",
3376
+ forcePathStyle: options.forcePathStyle ?? true,
3377
+ credentials: {
3378
+ accessKeyId: options.accessKeyId,
3379
+ secretAccessKey: options.secretAccessKey
3380
+ }
3381
+ });
3382
+ }
3383
+ var init_create_s3_compat_client = __esmMin((() => {}));
3384
+ //#endregion
3385
+ //#region ../../libs/shared/s3-client/src/index.ts
3386
+ var init_src$12 = __esmMin((() => {
3387
+ init_create_s3_compat_client();
3388
+ }));
3389
+ //#endregion
3348
3390
  //#region ../../libs/sdk/src/config.ts
3349
3391
  /**
3350
3392
  * Single seam in the SDK for reading `process.env`. Every downstream module
@@ -3375,6 +3417,7 @@ function resolveConfig(config) {
3375
3417
  s3Endpoint: config.s3?.endpoint ?? env["STACKBONE_S3_ENDPOINT"],
3376
3418
  s3Bucket: config.s3?.bucket ?? env["STACKBONE_S3_BUCKET"],
3377
3419
  s3Region: config.s3?.region ?? env["STACKBONE_S3_REGION"] ?? "us-east-1",
3420
+ s3ForcePathStyle: config.s3?.forcePathStyle ?? parseForcePathStyle(env["STACKBONE_S3_FORCE_PATH_STYLE"]),
3378
3421
  otelExporterOtlpEndpoint: config.otel?.exporterOtlpEndpoint ?? env["OTEL_EXPORTER_OTLP_ENDPOINT"],
3379
3422
  requireContract: env["STACKBONE_REQUIRE_CONTRACT"] !== "0",
3380
3423
  contractTtlMs: parseTtlMs(env["STACKBONE_CONTRACT_TTL_MS"]),
@@ -3394,7 +3437,9 @@ function parseTtlMs(raw) {
3394
3437
  if (!Number.isFinite(n) || n <= 0) return void 0;
3395
3438
  return n;
3396
3439
  }
3397
- var init_config$1 = __esmMin((() => {}));
3440
+ var init_config$1 = __esmMin((() => {
3441
+ init_src$12();
3442
+ }));
3398
3443
  //#endregion
3399
3444
  //#region ../../libs/sdk/src/result.ts
3400
3445
  function ok$1(data) {
@@ -3977,7 +4022,7 @@ var init_stackbone_signature = __esmMin((() => {
3977
4022
  }));
3978
4023
  //#endregion
3979
4024
  //#region ../../libs/shared/crypto/src/index.ts
3980
- var init_src$12 = __esmMin((() => {
4025
+ var init_src$11 = __esmMin((() => {
3981
4026
  init_secret_cipher();
3982
4027
  init_constant_time();
3983
4028
  init_workflow_signature();
@@ -4054,7 +4099,7 @@ function isDecision(value) {
4054
4099
  }
4055
4100
  var SIGNATURE_HEADER, DEFAULT_TOLERANCE_SECONDS, FUTURE_DRIFT_TOLERANCE_SECONDS;
4056
4101
  var init_verify = __esmMin((() => {
4057
- init_src$12();
4102
+ init_src$11();
4058
4103
  init_result$1();
4059
4104
  init_approval();
4060
4105
  SIGNATURE_HEADER = STACKBONE_SIGNATURE_HEADER;
@@ -4742,7 +4787,7 @@ var init_compile = __esmMin((() => {
4742
4787
  }));
4743
4788
  //#endregion
4744
4789
  //#region ../../libs/shared/prompt-compiler/src/index.ts
4745
- var init_src$11 = __esmMin((() => {
4790
+ var init_src$10 = __esmMin((() => {
4746
4791
  init_compile();
4747
4792
  }));
4748
4793
  //#endregion
@@ -4812,7 +4857,7 @@ function toIso$2(value) {
4812
4857
  }
4813
4858
  var PG_UNDEFINED_TABLE$4, PG_UNIQUE_VIOLATION$2, PromptsFacade;
4814
4859
  var init_prompts = __esmMin((() => {
4815
- init_src$11();
4860
+ init_src$10();
4816
4861
  init_result$1();
4817
4862
  PG_UNDEFINED_TABLE$4 = "42P01";
4818
4863
  PG_UNIQUE_VIOLATION$2 = "23505";
@@ -5933,7 +5978,7 @@ var init_jobs = __esmMin((() => {
5933
5978
  }));
5934
5979
  //#endregion
5935
5980
  //#region ../../libs/shared/rag-core/src/index.ts
5936
- var init_src$10 = __esmMin((() => {
5981
+ var init_src$9 = __esmMin((() => {
5937
5982
  init_result();
5938
5983
  init_limits();
5939
5984
  init_rag_contract();
@@ -6148,7 +6193,7 @@ function resolveRetrieveModel(request, resolved) {
6148
6193
  }
6149
6194
  var DEFAULT_BATCH_SIZE, RagModule;
6150
6195
  var init_rag = __esmMin((() => {
6151
- init_src$10();
6196
+ init_src$9();
6152
6197
  init_src$13();
6153
6198
  init_module_gate();
6154
6199
  init_result$1();
@@ -6318,7 +6363,7 @@ var init_rag = __esmMin((() => {
6318
6363
  //#region ../../libs/sdk/src/surfaces/agent-local/secrets/secrets.ts
6319
6364
  var SecretsFacade;
6320
6365
  var init_secrets = __esmMin((() => {
6321
- init_src$12();
6366
+ init_src$11();
6322
6367
  init_result$1();
6323
6368
  SecretsFacade = class {
6324
6369
  constructor(_resolved, _getDatabase) {
@@ -7123,35 +7168,6 @@ var init_call_connector = __esmMin((() => {
7123
7168
  connection = ((id) => connectorHandle(id));
7124
7169
  }));
7125
7170
  //#endregion
7126
- //#region ../../libs/shared/s3-client/src/lib/create-s3-compat-client.ts
7127
- /**
7128
- * The single S3-compatible client factory. Behind this seam it always sets
7129
- * `forcePathStyle: true` (MinIO and most S3-compatible backends do not support
7130
- * virtual-host buckets), falls back to `'us-east-1'` when the caller pins no
7131
- * region, and shapes the flat `accessKeyId` / `secretAccessKey` pair into the
7132
- * SDK's `credentials` object.
7133
- *
7134
- * Call sites pin their region explicitly: studio and the SDK pass the one they
7135
- * resolved, the emulator pins `'us-east-1'`.
7136
- */
7137
- function createS3CompatClient(options) {
7138
- return new S3Client({
7139
- endpoint: options.endpoint,
7140
- region: options.region ?? "us-east-1",
7141
- forcePathStyle: true,
7142
- credentials: {
7143
- accessKeyId: options.accessKeyId,
7144
- secretAccessKey: options.secretAccessKey
7145
- }
7146
- });
7147
- }
7148
- var init_create_s3_compat_client = __esmMin((() => {}));
7149
- //#endregion
7150
- //#region ../../libs/shared/s3-client/src/index.ts
7151
- var init_src$9 = __esmMin((() => {
7152
- init_create_s3_compat_client();
7153
- }));
7154
- //#endregion
7155
7171
  //#region ../../libs/sdk/src/surfaces/external/storage/storage.ts
7156
7172
  function stripEtag$1(etag) {
7157
7173
  return etag?.replace(/^"|"$/g, "");
@@ -7180,7 +7196,7 @@ function toStorageError(message, cause) {
7180
7196
  }
7181
7197
  var DEFAULT_SIGNED_URL_TTL_SECONDS, StorageModule, StorageBucket, S3_CREDENTIALS_MISSING_MESSAGE, S3_BUCKET_MISSING_MESSAGE, AGENT_ID_MISSING_MESSAGE;
7182
7198
  var init_storage = __esmMin((() => {
7183
- init_src$9();
7199
+ init_src$12();
7184
7200
  init_module_gate();
7185
7201
  init_result$1();
7186
7202
  DEFAULT_SIGNED_URL_TTL_SECONDS = 3600;
@@ -7216,7 +7232,8 @@ var init_storage = __esmMin((() => {
7216
7232
  endpoint,
7217
7233
  region,
7218
7234
  accessKeyId,
7219
- secretAccessKey
7235
+ secretAccessKey,
7236
+ forcePathStyle: this._resolved.s3ForcePathStyle
7220
7237
  });
7221
7238
  return ok$1({
7222
7239
  client: this._s3,
@@ -7732,7 +7749,7 @@ function createDatabaseProfilePersistence() {
7732
7749
  }
7733
7750
  var AGENT_IDENTITY, errMessage$23, BrowserAuthProfileStore;
7734
7751
  var init_browser_profile_store = __esmMin((() => {
7735
- init_src$12();
7752
+ init_src$11();
7736
7753
  init_result$1();
7737
7754
  init_ambient_client();
7738
7755
  init_browser_profile_state();
@@ -8407,7 +8424,7 @@ var init_src$8 = __esmMin((() => {
8407
8424
  init_browser_tools();
8408
8425
  init_browser_mode_resolver();
8409
8426
  init_browser_profile_store();
8410
- init_src$12();
8427
+ init_src$11();
8411
8428
  init_ambient_client();
8412
8429
  }));
8413
8430
  //#endregion
@@ -10216,6 +10233,7 @@ var resolveCorsAllowlist = (input) => {
10216
10233
  };
10217
10234
  //#endregion
10218
10235
  //#region ../../libs/runtime/workspace-runtime/src/lib/server/agent-config.ts
10236
+ init_src$13();
10219
10237
  var StudioAgentConfigValidationError = class extends Error {
10220
10238
  constructor(errors) {
10221
10239
  super("agent_config_invalid");
@@ -10230,15 +10248,6 @@ var StudioAgentConfigVersionNotFoundError = class extends Error {
10230
10248
  }
10231
10249
  };
10232
10250
  /**
10233
- * Literal location of the config JSON Schema the dev build extracts from the
10234
- * creator's `config.schema.ts`, relative to the project root. Shared artifact
10235
- * contract across the whole feature (`config_schema_dynamic_config`): the CLI
10236
- * extractor writes it, the SDK type codegen reads it, and the emulator (here)
10237
- * resolves it for the Config editor + write validation. Kept as a literal so the
10238
- * emulator never imports `apps/cli`.
10239
- */
10240
- var CONFIG_SCHEMA_ARTIFACT_PATH = ".well-known/config/v1/config-schema.json";
10241
- /**
10242
10251
  * Read the extracted config JSON Schema off disk for a given project root.
10243
10252
  *
10244
10253
  * Reads `<projectRoot>/.well-known/config/v1/config-schema.json` (the build
@@ -10434,15 +10443,17 @@ var init_well_known = __esmMin((() => {
10434
10443
  }));
10435
10444
  //#endregion
10436
10445
  //#region ../../libs/runtime/workspace-runtime/src/lib/server/box-auth-gate.ts
10437
- init_src$12();
10446
+ init_src$11();
10438
10447
  init_well_known();
10439
10448
  var PROXY_CHALLENGE_PATH = "/api/proxy-challenge";
10449
+ var CONTROL_PLANE_IDENTITY_PATH = "/api/control-plane-identity";
10440
10450
  var PUBLIC_PREFIXES = [
10441
10451
  "/api/contract",
10442
10452
  "/api/health",
10443
10453
  "/live",
10444
10454
  "/health",
10445
10455
  PROXY_CHALLENGE_PATH,
10456
+ CONTROL_PLANE_IDENTITY_PATH,
10446
10457
  "/api/connect/callback",
10447
10458
  "/api/connect/connected",
10448
10459
  "/api/connect/token",
@@ -10489,6 +10500,14 @@ var rejectToken = (c) => c.json({
10489
10500
  code: "invalid_token",
10490
10501
  message: "Identity token is missing, expired, or invalid."
10491
10502
  }, 401);
10503
+ var rejectTokenTenant = (c) => c.json({
10504
+ code: "invalid_token_tenant",
10505
+ message: "Identity token is valid, but it was issued for a different organization or installation than this box serves."
10506
+ }, 401);
10507
+ var rejectUntaught = (c) => c.json({
10508
+ code: "box_not_registered",
10509
+ message: "This box has not been told which installation it answers for, so it cannot accept any identity token yet. Register it from the control plane (or re-run `stackbone link`) and try again."
10510
+ }, 401);
10492
10511
  var BEARER_RE = /^Bearer\s+(.+)$/i;
10493
10512
  var extractBearer = (header) => {
10494
10513
  if (!header) return null;
@@ -10509,9 +10528,12 @@ var extractIdentity = (payload) => {
10509
10528
  };
10510
10529
  /**
10511
10530
  * Build the identity-token verifier ONCE, so the JWKS resolver (and its in-memory
10512
- * cache) lives for the process instead of being rebuilt per request. Returns the
10513
- * verified identity, or `null` for every failure mode — missing, malformed,
10514
- * expired, wrong key, wrong issuer/audience, or minted for another tenant.
10531
+ * cache) lives for the process instead of being rebuilt per request.
10532
+ *
10533
+ * Signature + expiry + issuer/audience are verified BEFORE any tenant comparison,
10534
+ * so a token that failed verification is never classified from claims nobody
10535
+ * proved: an attacker cannot mint an unsigned token with a foreign `org` and have
10536
+ * the box answer "wrong tenant", which would confirm the box does not serve it.
10515
10537
  */
10516
10538
  var createIdentityTokenVerifier = (options) => {
10517
10539
  const getKey = options.createJwks?.() ?? createRemoteJWKSet(new URL(options.jwksUrl), {
@@ -10520,7 +10542,10 @@ var createIdentityTokenVerifier = (options) => {
10520
10542
  });
10521
10543
  return async (c) => {
10522
10544
  const token = extractBearer(c.req.header("authorization"));
10523
- if (!token) return null;
10545
+ if (!token) return {
10546
+ ok: false,
10547
+ reason: "absent"
10548
+ };
10524
10549
  let payload;
10525
10550
  try {
10526
10551
  ({payload} = await jwtVerify(token, getKey, {
@@ -10528,13 +10553,41 @@ var createIdentityTokenVerifier = (options) => {
10528
10553
  audience: options.audience
10529
10554
  }));
10530
10555
  } catch {
10531
- return null;
10556
+ return {
10557
+ ok: false,
10558
+ reason: "unverifiable"
10559
+ };
10532
10560
  }
10533
10561
  const identity = extractIdentity(payload);
10534
- if (!identity) return null;
10535
- if (identity.org !== options.expectedOrg) return null;
10536
- if (identity.install !== void 0 && identity.install !== options.expectedInstall) return null;
10537
- return identity;
10562
+ if (!identity) return {
10563
+ ok: false,
10564
+ reason: "unverifiable"
10565
+ };
10566
+ if (identity.org === void 0) return {
10567
+ ok: false,
10568
+ reason: "unverifiable"
10569
+ };
10570
+ const answersFor = await options.answersFor();
10571
+ if (!answersFor) return {
10572
+ ok: false,
10573
+ reason: "untaught"
10574
+ };
10575
+ if (identity.org !== answersFor.organizationId) return {
10576
+ ok: false,
10577
+ reason: "wrong_tenant"
10578
+ };
10579
+ if (identity.install === void 0) return {
10580
+ ok: false,
10581
+ reason: "unverifiable"
10582
+ };
10583
+ if (identity.install !== answersFor.installationId) return {
10584
+ ok: false,
10585
+ reason: "wrong_tenant"
10586
+ };
10587
+ return {
10588
+ ok: true,
10589
+ identity
10590
+ };
10538
10591
  };
10539
10592
  };
10540
10593
  /**
@@ -10568,9 +10621,12 @@ var createBoxAuthGate = (options) => {
10568
10621
  const proxySecret = options.proxyAuth?.secret;
10569
10622
  const verifyIdentityToken = options.jwtAuth ? createIdentityTokenVerifier(options.jwtAuth) : void 0;
10570
10623
  if (!proxySecret && !verifyIdentityToken) return async (_c, next) => next();
10571
- const reject = (c) => {
10572
- if (verifyIdentityToken && extractBearer(c.req.header("authorization")) || !proxySecret) return rejectToken(c);
10573
- return rejectSignature(c);
10624
+ const reject = (c, check) => {
10625
+ const reason = check?.ok === false ? check.reason : void 0;
10626
+ if (reason === "untaught") return rejectUntaught(c);
10627
+ if (reason === "wrong_tenant") return rejectTokenTenant(c);
10628
+ if (reason === "unverifiable") return rejectToken(c);
10629
+ return proxySecret ? rejectSignature(c) : rejectToken(c);
10574
10630
  };
10575
10631
  return async (c, next) => {
10576
10632
  if (c.req.method === "OPTIONS") return next();
@@ -10579,23 +10635,21 @@ var createBoxAuthGate = (options) => {
10579
10635
  c.set(AUTHENTICATED_CALLER_VAR, { provenance: "proxy" });
10580
10636
  return next();
10581
10637
  }
10582
- if (verifyIdentityToken) {
10583
- const identity = await verifyIdentityToken(c);
10584
- if (identity) {
10585
- c.set(AUTHENTICATED_CALLER_VAR, {
10586
- provenance: "identity",
10587
- ...identity
10588
- });
10589
- return next();
10590
- }
10638
+ const check = await verifyIdentityToken?.(c);
10639
+ if (check?.ok) {
10640
+ c.set(AUTHENTICATED_CALLER_VAR, {
10641
+ provenance: "identity",
10642
+ ...check.identity
10643
+ });
10644
+ return next();
10591
10645
  }
10592
- return reject(c);
10646
+ return reject(c, check);
10593
10647
  };
10594
10648
  };
10595
10649
  //#endregion
10596
10650
  //#region ../../libs/runtime/workspace-runtime/src/lib/server/contract.ts
10597
10651
  init_src$13();
10598
- var STACKBONE_CLI_BUILD_VERSION = "0.3.2";
10652
+ var STACKBONE_CLI_BUILD_VERSION = "0.3.3";
10599
10653
  /**
10600
10654
  * Capabilities the local emulator advertises on `GET /api/contract`, derived
10601
10655
  * from the route registry above so the advertised set and the mounted routes
@@ -10994,7 +11048,7 @@ async function runLogTail(stream, filters, startTailer) {
10994
11048
  }
10995
11049
  //#endregion
10996
11050
  //#region ../../libs/runtime/workspace-runtime/src/lib/handlers/actor.ts
10997
- init_src$12();
11051
+ init_src$11();
10998
11052
  function readActorUserId(c) {
10999
11053
  const verified = readVerifiedIdentity(c);
11000
11054
  if (verified?.userId) return verified.userId;
@@ -11055,7 +11109,7 @@ var requireCapability = (capability) => {
11055
11109
  };
11056
11110
  //#endregion
11057
11111
  //#region ../../libs/runtime/workspace-runtime/src/lib/handlers/prompts-handlers.ts
11058
- init_src$11();
11112
+ init_src$10();
11059
11113
  init_src$13();
11060
11114
  var PG_UNIQUE_VIOLATION$1 = "23505";
11061
11115
  var withClient$3 = async (opts, fn) => {
@@ -11346,8 +11400,8 @@ var createPromptsRouter = (opts) => {
11346
11400
  };
11347
11401
  //#endregion
11348
11402
  //#region ../../libs/runtime/workspace-runtime/src/lib/services/storage.ts
11349
- init_src$10();
11350
11403
  init_src$9();
11404
+ init_src$12();
11351
11405
  var ObjectNotFoundError = class extends Error {
11352
11406
  constructor(message = "object_not_found") {
11353
11407
  super(message);
@@ -11358,7 +11412,8 @@ var buildClient = (options) => createS3CompatClient({
11358
11412
  endpoint: options.endpoint,
11359
11413
  region: options.region || "us-east-1",
11360
11414
  accessKeyId: options.accessKey,
11361
- secretAccessKey: options.secretKey
11415
+ secretAccessKey: options.secretKey,
11416
+ ...options.forcePathStyle === void 0 ? {} : { forcePathStyle: options.forcePathStyle }
11362
11417
  });
11363
11418
  var listBuckets$1 = async (options) => ({ items: [{
11364
11419
  name: options.bucket,
@@ -11614,7 +11669,7 @@ var createStorageRouter = (storageOptions) => {
11614
11669
  };
11615
11670
  //#endregion
11616
11671
  //#region ../../libs/runtime/workspace-runtime/src/lib/services/secrets.ts
11617
- init_src$12();
11672
+ init_src$11();
11618
11673
  init_src$13();
11619
11674
  var StudioSecretNotFoundError = class extends Error {
11620
11675
  constructor() {
@@ -24835,7 +24890,7 @@ var init_resume_interrupt = __esmMin((() => {
24835
24890
  }));
24836
24891
  //#endregion
24837
24892
  //#region ../../libs/runtime/workspace-runtime/src/lib/evals/eval-approval-answer.ts
24838
- init_src$12();
24893
+ init_src$11();
24839
24894
  init_resume_interrupt();
24840
24895
  init_guardrail_park();
24841
24896
  /** The route the box records a decision on. */
@@ -27204,7 +27259,7 @@ var createSessionsRouter = (deps) => {
27204
27259
  };
27205
27260
  //#endregion
27206
27261
  //#region ../../libs/runtime/workspace-runtime/src/lib/handlers/approval-callback.ts
27207
- init_src$12();
27262
+ init_src$11();
27208
27263
  init_src$13();
27209
27264
  var buildCallbackPayload = (approval, decision) => ({
27210
27265
  status: decision.decision === "approve" ? "approved" : "rejected",
@@ -28312,7 +28367,7 @@ var mapRunToRagJob = (run) => ({
28312
28367
  });
28313
28368
  //#endregion
28314
28369
  //#region ../../libs/runtime/workspace-runtime/src/lib/handlers/rag-handlers.ts
28315
- init_src$10();
28370
+ init_src$9();
28316
28371
  init_src$13();
28317
28372
  var RAG_DIMENSIONS$1 = 1536;
28318
28373
  var RAG_DISTANCE$1 = "cosine";
@@ -28590,7 +28645,7 @@ var init_readiness$1 = __esmMin((() => {
28590
28645
  }));
28591
28646
  //#endregion
28592
28647
  //#region ../../libs/runtime/workspace-runtime/src/lib/handlers/rag-write-handlers.ts
28593
- init_src$10();
28648
+ init_src$9();
28594
28649
  init_readiness$1();
28595
28650
  var createMinioRagWriteStorage = (storage) => ({
28596
28651
  putManagedObject: async (key, body, contentType) => {
@@ -28856,7 +28911,7 @@ var basename$1 = (key) => {
28856
28911
  //#endregion
28857
28912
  //#region ../../libs/runtime/workspace-runtime/src/lib/handlers/rag-router.ts
28858
28913
  init_src$13();
28859
- init_src$10();
28914
+ init_src$9();
28860
28915
  /**
28861
28916
  * Builds the Hono router mounted at `/api/rag` by the emulator server. Studio RAG
28862
28917
  * explorer + managed WRITE surface (V9 + feature 30, F76/F5). Reads/writes the
@@ -32412,7 +32467,7 @@ var init_src$1 = __esmMin((() => {
32412
32467
  init_token_cache_key();
32413
32468
  init_get_token();
32414
32469
  init_src$3();
32415
- init_src$12();
32470
+ init_src$11();
32416
32471
  init_schema();
32417
32472
  init_grant_refresh_ports();
32418
32473
  init_grant_refresh_runtime();
@@ -32478,7 +32533,7 @@ function errorMessage$1(err) {
32478
32533
  }
32479
32534
  //#endregion
32480
32535
  //#region ../../libs/runtime/workspace-runtime/src/lib/handlers/connect-handlers.ts
32481
- init_src$12();
32536
+ init_src$11();
32482
32537
  init_src$1();
32483
32538
  init_src$3();
32484
32539
  init_src$2();
@@ -40171,7 +40226,7 @@ var init_src = __esmMin((() => {
40171
40226
  //#region ../../libs/runtime/workspace-runtime/src/lib/catalog/system-entries.ts
40172
40227
  var RESERVED_ROLES, isSystemEntry;
40173
40228
  var init_system_entries = __esmMin((() => {
40174
- init_src$10();
40229
+ init_src$9();
40175
40230
  init_src();
40176
40231
  RESERVED_ROLES = {
40177
40232
  agent: new Set([RAG_INGEST_ROLE]),
@@ -41170,6 +41225,7 @@ var init_connections = __esmMin((() => {
41170
41225
  s3SecretKey: "STACKBONE_S3_SECRET_KEY",
41171
41226
  s3Bucket: "STACKBONE_S3_BUCKET",
41172
41227
  s3Region: "STACKBONE_S3_REGION",
41228
+ s3ForcePathStyle: "STACKBONE_S3_FORCE_PATH_STYLE",
41173
41229
  agentConfig: "STACKBONE_AGENT_CONFIG",
41174
41230
  port: "PORT",
41175
41231
  mode: "STACKBONE_MODE",
@@ -41186,6 +41242,7 @@ var init_connections = __esmMin((() => {
41186
41242
  [STACKBONE_ENV_KEYS.s3SecretKey]: injection.services.s3SecretKey,
41187
41243
  [STACKBONE_ENV_KEYS.s3Bucket]: injection.services.s3Bucket,
41188
41244
  [STACKBONE_ENV_KEYS.s3Region]: injection.services.s3Region,
41245
+ [STACKBONE_ENV_KEYS.s3ForcePathStyle]: String(injection.services.s3ForcePathStyle),
41189
41246
  [STACKBONE_ENV_KEYS.port]: String(injection.agentPort),
41190
41247
  [STACKBONE_ENV_KEYS.mode]: options.mode,
41191
41248
  [STACKBONE_ENV_KEYS.secretKey]: options.secretKey
@@ -42795,7 +42852,7 @@ async function failApprovalHookStep(params) {
42795
42852
  var deepAgentBridgeLogger, deriveWorkflowSteps, degradedReasonFor, resolveWorkflowRefs, applySdkContractEnv, extractWorldRunIdFromFlowBody, loadProjectRuntime, mountWorkflowRuntime;
42796
42853
  var init_runtime_mount = __esmMin((() => {
42797
42854
  init_connections();
42798
- init_src$10();
42855
+ init_src$9();
42799
42856
  init_src();
42800
42857
  init_registry$1();
42801
42858
  init_readiness$1();
@@ -44930,7 +44987,8 @@ var init_mount = __esmMin((() => {
44930
44987
  [STACKBONE_ENV_KEYS.s3AccessKey, services.s3AccessKey],
44931
44988
  [STACKBONE_ENV_KEYS.s3SecretKey, services.s3SecretKey],
44932
44989
  [STACKBONE_ENV_KEYS.s3Bucket, services.s3Bucket],
44933
- [STACKBONE_ENV_KEYS.s3Region, services.s3Region]
44990
+ [STACKBONE_ENV_KEYS.s3Region, services.s3Region],
44991
+ [STACKBONE_ENV_KEYS.s3ForcePathStyle, String(services.s3ForcePathStyle)]
44934
44992
  ];
44935
44993
  const seeded = [];
44936
44994
  for (const [name, value] of wanted) if (env[name] === void 0 || env[name] === "") {
@@ -45017,8 +45075,8 @@ var init_mount = __esmMin((() => {
45017
45075
  //#endregion
45018
45076
  //#region ../../libs/runtime/workspace-runtime/src/lib/server/emulator-server.ts
45019
45077
  init_src$13();
45020
- init_src$12();
45021
- init_src$10();
45078
+ init_src$11();
45079
+ init_src$9();
45022
45080
  init_workflow_guardrails();
45023
45081
  init_src$1();
45024
45082
  init_recurring_jobs_registry();
@@ -45039,6 +45097,28 @@ init_well_known();
45039
45097
  * agents run IN-PROCESS behind it, not on a port of their own.
45040
45098
  */
45041
45099
  var DEV_INSTALLATION_ID = "local-dev";
45100
+ /**
45101
+ * STA-594 — the teach call's body: the control plane's installation id and the
45102
+ * organization it belongs to. `null` when the body is not that, so a signed call
45103
+ * with a mistyped field is refused loudly instead of recording an empty identity
45104
+ * the box would then answer for.
45105
+ */
45106
+ var readControlPlaneIdentityBody = (raw) => {
45107
+ let parsed;
45108
+ try {
45109
+ parsed = JSON.parse(raw);
45110
+ } catch {
45111
+ return null;
45112
+ }
45113
+ if (!parsed || typeof parsed !== "object") return null;
45114
+ const { installationId, organizationId } = parsed;
45115
+ if (typeof installationId !== "string" || installationId.trim() === "") return null;
45116
+ if (typeof organizationId !== "string" || organizationId.trim() === "") return null;
45117
+ return {
45118
+ installationId,
45119
+ organizationId
45120
+ };
45121
+ };
45042
45122
  var buildEmulatorApp = (opts, hooks = {}) => {
45043
45123
  const app = new Hono();
45044
45124
  const agentBaseUrl = `http://127.0.0.1:${opts.injection.agentPort}`;
@@ -45072,7 +45152,7 @@ var buildEmulatorApp = (opts, hooks = {}) => {
45072
45152
  }));
45073
45153
  if (opts.proxyChallenge?.secret) {
45074
45154
  const challengeSecret = opts.proxyChallenge.secret;
45075
- app.get(PROXY_CHALLENGE_PATH, (c) => {
45155
+ app.get(PROXY_CHALLENGE_PATH, async (c) => {
45076
45156
  const url = new URL(c.req.url);
45077
45157
  const ok = verifyStackboneSignature({
45078
45158
  secret: challengeSecret,
@@ -45085,10 +45165,43 @@ var buildEmulatorApp = (opts, hooks = {}) => {
45085
45165
  });
45086
45166
  const body = {
45087
45167
  ok,
45088
- identityVerification: opts.jwtAuth ? "armed" : "unarmed"
45168
+ identityVerification: !opts.jwtAuth ? "unarmed" : await opts.jwtAuth.answersFor() ? "armed" : "untaught"
45089
45169
  };
45090
45170
  return c.json(body, ok ? 200 : 401);
45091
45171
  });
45172
+ const learn = opts.learnControlPlaneIdentity;
45173
+ if (learn) app.post(CONTROL_PLANE_IDENTITY_PATH, async (c) => {
45174
+ const url = new URL(c.req.url);
45175
+ const raw = await c.req.raw.clone().text();
45176
+ if (!verifyStackboneSignature({
45177
+ secret: challengeSecret,
45178
+ header: c.req.header("stackbone-signature"),
45179
+ payload: proxyRequestPayload({
45180
+ method: "POST",
45181
+ path: url.pathname + url.search,
45182
+ body: raw
45183
+ })
45184
+ })) return c.json({
45185
+ ok: false,
45186
+ code: "invalid_signature"
45187
+ }, 401);
45188
+ const learned = readControlPlaneIdentityBody(raw);
45189
+ if (!learned) return c.json({
45190
+ ok: false,
45191
+ code: "invalid_body",
45192
+ message: "Expected a JSON body naming installationId and organizationId, both non-empty strings."
45193
+ }, 400);
45194
+ try {
45195
+ await learn(learned);
45196
+ } catch (err) {
45197
+ return c.json({
45198
+ ok: false,
45199
+ code: "not_recorded",
45200
+ message: err instanceof Error ? err.message : String(err)
45201
+ }, 500);
45202
+ }
45203
+ return c.json({ ok: true });
45204
+ });
45092
45205
  }
45093
45206
  app.get("/api/contract", (c) => c.json(buildContractResponse()));
45094
45207
  app.get("/", (c) => c.html(renderStudioStub({
@@ -45194,6 +45307,7 @@ var buildEmulatorApp = (opts, hooks = {}) => {
45194
45307
  secretKey: opts.injection.services.s3SecretKey,
45195
45308
  bucket: opts.injection.services.s3Bucket,
45196
45309
  region: opts.injection.services.s3Region,
45310
+ forcePathStyle: opts.injection.services.s3ForcePathStyle,
45197
45311
  agentId: opts.agent.slug
45198
45312
  };
45199
45313
  app.route("/api/storage", createStorageRouter(storageOptions));
@@ -45213,12 +45327,12 @@ var buildEmulatorApp = (opts, hooks = {}) => {
45213
45327
  invocations: opts.invocations,
45214
45328
  ...hooks.resolveRagIngestTrigger ? { resolveRagIngestTrigger: hooks.resolveRagIngestTrigger } : {}
45215
45329
  }));
45216
- const secretsOptions = { connectionString: opts.injection.services.postgresUrl };
45217
- app.route("/api/secrets", createSecretsRouter(secretsOptions));
45218
- const privilegedSecretsOptions = {
45330
+ const secretsOptions = {
45219
45331
  connectionString: opts.injection.services.postgresUrl,
45220
45332
  ...opts.secretKey ? { cipherKeyBase64: opts.secretKey } : {}
45221
45333
  };
45334
+ app.route("/api/secrets", createSecretsRouter(secretsOptions));
45335
+ const privilegedSecretsOptions = secretsOptions;
45222
45336
  app.route("/api/model-provider", createModelProviderRouter(privilegedSecretsOptions, { ...opts.bootEnv ?? {} }, { ...hooks.retryDegradedDeepAgents ? { onProviderSaved: hooks.retryDegradedDeepAgents } : {} }));
45223
45337
  app.route("/api/browser-provider", createBrowserProviderRouter(privilegedSecretsOptions));
45224
45338
  app.route("/api/browser-profiles", createBrowserProfilesRouter({ connectionString: opts.injection.services.postgresUrl }));
@@ -45935,7 +46049,8 @@ var buildDevServices = (ports) => ({
45935
46049
  s3AccessKey: DEV_SERVICE_CREDENTIALS.s3AccessKey,
45936
46050
  s3SecretKey: DEV_SERVICE_CREDENTIALS.s3SecretKey,
45937
46051
  s3Bucket: DEV_SERVICE_CREDENTIALS.s3Bucket,
45938
- s3Region: DEV_SERVICE_CREDENTIALS.s3Region
46052
+ s3Region: DEV_SERVICE_CREDENTIALS.s3Region,
46053
+ s3ForcePathStyle: true
45939
46054
  });
45940
46055
  var buildDevRedisUrl = (redisPort) => `redis://127.0.0.1:${redisPort}`;
45941
46056
  var DEV_AGENT_SECRET_KEY = createHash("sha256").update(STUDIO_LOCAL_DEV_ENCRYPTION_KEY).digest("base64");
@@ -46248,7 +46363,7 @@ var calls_workflow_ts_default = "import { z } from \"zod\";\nimport { streamDeep
46248
46363
  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";
46249
46364
  //#endregion
46250
46365
  //#region src/services/scaffold/template-loader.ts
46251
- var SDK_TEMPLATE_VERSION = "0.3.0";
46366
+ var SDK_TEMPLATE_VERSION = "0.3.1";
46252
46367
  var PREFIX = "./templates/";
46253
46368
  var templates = new Map(Object.entries(/* @__PURE__ */ Object.assign({
46254
46369
  "./templates/deep-agents/index.ts.tmpl": index_ts_default,
@@ -46534,10 +46649,10 @@ var createIdentityTokenProvider = (opts) => {
46534
46649
  let token = null;
46535
46650
  return {
46536
46651
  current: () => token,
46537
- refresh: async () => {
46652
+ refresh: async (target) => {
46538
46653
  let minted;
46539
46654
  try {
46540
- minted = await opts.mint();
46655
+ minted = await opts.mint(target);
46541
46656
  } catch {
46542
46657
  minted = null;
46543
46658
  }
@@ -46549,34 +46664,28 @@ var createIdentityTokenProvider = (opts) => {
46549
46664
  //#endregion
46550
46665
  //#region src/services/identity-token.service.ts
46551
46666
  /** Route the control plane mints the box-facing identity JWT on (better-auth jwt plugin). */
46552
- var IDENTITY_TOKEN_PATH = "/api/auth/token";
46667
+ var IDENTITY_TOKEN_PATH = "/api/auth/token/organization";
46553
46668
  /**
46554
46669
  * Builds the provider for `ctx`, which MUST be the control-plane context — the
46555
46670
  * token is minted by the control plane, never by the box it is presented to.
46556
- */
46557
- var createIdentityTokenService = (ctx) => createIdentityTokenProvider({ mint: async () => {
46558
- return (await ctx.http.get(IDENTITY_TOKEN_PATH))?.token;
46559
- } });
46560
- /**
46561
- * The organization id a minted identity token carries, or `null` when it carries
46562
- * none (or is not a readable JWT).
46563
46671
  *
46564
- * Read WITHOUT verifying, on purpose: the signature is the box's business, not
46565
- * ours. All the CLI needs is which tenant the control plane minted for, so it can
46566
- * refuse to address a box that serves a different one instead of sending a token
46567
- * the box will reject. `/api/auth/token` is session-scoped and mints for the
46568
- * caller's earliest-joined organization, so an operator who belongs to several
46569
- * gets a token for one of them and 401s on the rest.
46570
- */
46571
- var identityTokenOrganizationId = (token) => {
46572
- const payload = token.split(".")[1];
46573
- if (!payload) return null;
46574
- try {
46575
- const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
46576
- return typeof claims.org === "string" && claims.org.length > 0 ? claims.org : null;
46577
- } catch {
46578
- return null;
46579
- }
46672
+ * `organization` is the slug of the organization the target box runs in, and
46673
+ * `installation` is the control plane's id for that box. A caller with no
46674
+ * membership there, or naming a box that is not one of that organization's
46675
+ * installations, gets no token: the control plane refuses, `refresh()` resolves
46676
+ * to `null`, and nothing is substituted.
46677
+ */
46678
+ var createIdentityTokenService = (ctx, organization, installation) => {
46679
+ const provider = createIdentityTokenProvider({ mint: async (target) => {
46680
+ return (await ctx.http.get(`${IDENTITY_TOKEN_PATH}?org=${encodeURIComponent(target.organization)}&install=${encodeURIComponent(target.installation)}`))?.token;
46681
+ } });
46682
+ return {
46683
+ current: () => provider.current(),
46684
+ refresh: () => provider.refresh({
46685
+ organization,
46686
+ installation
46687
+ })
46688
+ };
46580
46689
  };
46581
46690
  //#endregion
46582
46691
  //#region src/services/organization.service.ts
@@ -46733,28 +46842,6 @@ var directoryLookupFailed = (installationId, cause) => {
46733
46842
  });
46734
46843
  };
46735
46844
  /**
46736
- * Refuse a box that serves a DIFFERENT organization than the one the minted
46737
- * identity token names, before a single call is addressed to it.
46738
- *
46739
- * `GET /api/auth/token` is session-scoped, not organization-scoped: it mints one
46740
- * `org` claim per account, the earliest-joined membership. The box hard-rejects a
46741
- * token whose `org` is not its own tenant, so for an operator who belongs to more
46742
- * than one organization every box outside that first one answers 401 — and the
46743
- * retry cannot help, because the re-mint asks the same endpoint and gets the same
46744
- * token back. Left alone the operator reads "Run `stackbone login`", a remedy
46745
- * that can never work.
46746
- *
46747
- * Only a DEFINITE mismatch refuses. A token with no `org` claim (or one this
46748
- * build cannot read) falls through to the box's own answer rather than inventing
46749
- * a second verdict about a shape we do not understand.
46750
- */
46751
- var assertIdentityServesOrg = async (ctx, token, org, installationId) => {
46752
- const mintedFor = identityTokenOrganizationId(token);
46753
- if (mintedFor === null || mintedFor === org.id) return;
46754
- const owner = (await getMyOrganizations(ctx).catch(() => [])).find((o) => o.id === mintedFor);
46755
- throwAuth(`Agent ${installationId} belongs to organization "${org.slug}", but your identity credential is minted for ${owner ? `"${owner.slug}"` : "another organization"}, and the agent's box refuses a credential from anywhere else.`, "Stackbone mints one identity credential per account, for the organization you joined first. Reaching a box in another organization is not supported yet.");
46756
- };
46757
- /**
46758
46845
  * Resolves the full studio routing context a surface verb needs: the
46759
46846
  * installation it targets (`resolveAgentTarget`) plus the datapath (the
46760
46847
  * customer's own box vs the running local emulator) and the effective
@@ -46846,10 +46933,8 @@ var resolveStudioTarget = async (ctx, flagValue, deps = {}) => {
46846
46933
  throw directoryLookupFailed(installationId, err);
46847
46934
  }
46848
46935
  if (!runtimeUrl) return throwRuntimeNotConfigured(installationId);
46849
- const identity = makeIdentity(ctx);
46850
- const token = await identity.refresh();
46851
- if (!token) return throwAuth(`Agent ${installationId} is registered, but this session could not mint the credential it requires.`, "Run `stackbone login` to refresh your session, then retry.");
46852
- await assertIdentityServesOrg(ctx, token, org, installationId);
46936
+ const identity = makeIdentity(ctx, org.slug, installationId);
46937
+ if (!await identity.refresh()) return throwAuth(`Agent ${installationId} is registered in organization "${org.slug}", but the control plane did not return the credential its box requires for that organization.`, `Retry — if the control plane is unreachable or erroring, that is the likeliest cause and nothing here needs fixing. If it keeps failing, run \`stackbone login\` to refresh your session; and if that does not help either, check you are still a member of "${org.slug}" with \`stackbone organization list\`.`);
46853
46938
  const http = makeAdapter({
46854
46939
  apiUrl: runtimeUrl,
46855
46940
  tokenStore: ctx.tokenStore,
@@ -47818,10 +47903,19 @@ var buildWorkspaceBundle = async (options) => {
47818
47903
  migrationsDir: options.migrationsDir,
47819
47904
  outDir
47820
47905
  });
47906
+ const hasConfigSchema = options.hasConfigSchema ?? await exists$1(resolve(projectRoot, "config.schema.ts"));
47907
+ if (hasConfigSchema) {
47908
+ const configSchema = await options.resolveConfigSchema?.() ?? null;
47909
+ if (configSchema) {
47910
+ const artifactPath = resolve(outDir, CONFIG_SCHEMA_ARTIFACT_PATH);
47911
+ await mkdir(dirname(artifactPath), { recursive: true });
47912
+ await writeFile(artifactPath, JSON.stringify(configSchema), "utf8");
47913
+ } else logger?.warn(`This workspace declares ${CONFIG_SCHEMA_SOURCE}, but no config JSON Schema was produced for the bundle. Studio's config editor will fall back to a raw JSON field and writes will not be validated.`);
47914
+ }
47821
47915
  const manifest = deriveWorkspaceManifest({
47822
47916
  workspace,
47823
47917
  workflowManifest,
47824
- hasConfigSchema: options.hasConfigSchema ?? await exists$1(resolve(projectRoot, "config.schema.ts")),
47918
+ hasConfigSchema,
47825
47919
  defaults: options.defaults,
47826
47920
  deepAgentExternals
47827
47921
  });
@@ -47883,6 +47977,181 @@ var resolveAgentDbConfig = (agentRoot) => {
47883
47977
  };
47884
47978
  };
47885
47979
  //#endregion
47980
+ //#region src/dev/extract-config-schema.ts
47981
+ /** Directory the extracted artifact lives in, relative to the workspace root.
47982
+ * A subdir of its own, parallel to the workflow well-known tree. */
47983
+ var CONFIG_WELL_KNOWN_DIR = ".well-known/config/v1";
47984
+ /** The shipped artifact: the single JSON Schema (JSON only, no live sidecar). */
47985
+ var CONFIG_SCHEMA_FILE = "config-schema.json";
47986
+ /** The intermediate esbuild bundle (harmless to leave behind, like the workflow
47987
+ * sidecar). NOT the deliverable — `config-schema.json` is. */
47988
+ var CONFIG_SCHEMA_BUNDLE = "config-schema.mjs";
47989
+ /**
47990
+ * Extract the workspace's single config JSON Schema. Defensive by contract: an
47991
+ * absent `config.schema.ts` degrades to `null` SILENTLY (the editor stays
47992
+ * free-form); a DECLARED-but-inconvertible schema degrades to `null` plus one
47993
+ * noisy warning; and a total bundle failure degrades to `null` plus one coarse
47994
+ * warning. It NEVER throws — a broken config schema must not break the dev build.
47995
+ */
47996
+ var extractConfigSchema = async (projectRoot, opts = {}) => {
47997
+ const loader = opts.loader ?? defaultConfigSchemaLoader;
47998
+ const warnings = [];
47999
+ const emit = (msg) => {
48000
+ warnings.push(msg);
48001
+ opts.logger?.warn(msg);
48002
+ };
48003
+ let loaded;
48004
+ try {
48005
+ loaded = await loader(projectRoot);
48006
+ } catch (err) {
48007
+ emit(`[config-schema] config schema extraction bundle failed; no config schema is available — the Config editor will fall back to a raw JSON field. Cause: ${errMessage$2(err)}`);
48008
+ return {
48009
+ schema: null,
48010
+ warnings
48011
+ };
48012
+ }
48013
+ if (loaded.configSchema == null) return {
48014
+ schema: null,
48015
+ warnings
48016
+ };
48017
+ try {
48018
+ return {
48019
+ schema: loaded.toJsonSchema(loaded.configSchema),
48020
+ warnings
48021
+ };
48022
+ } catch (err) {
48023
+ emit(`[config-schema] config.schema.ts could not be converted to JSON Schema — the Config editor will fall back to a raw JSON field. Cause: ${errMessage$2(err)}`);
48024
+ return {
48025
+ schema: null,
48026
+ warnings
48027
+ };
48028
+ }
48029
+ };
48030
+ /**
48031
+ * Extract the config schema and WRITE the artifact. When the schema is non-null,
48032
+ * writes `<projectRoot>/.well-known/config/v1/config-schema.json` (compact JSON,
48033
+ * like `manifest.json`) and returns `written: true`. When the schema is null
48034
+ * (absent or inconvertible), REMOVES any stale `config-schema.json` so a deleted
48035
+ * or broken schema does not leave a phantom artifact, and returns `written:
48036
+ * false`. Never throws (mirrors `extractConfigSchema`).
48037
+ */
48038
+ var buildConfigSchema = async (projectRoot, opts = {}) => {
48039
+ const { schema, warnings } = await extractConfigSchema(projectRoot, opts);
48040
+ const outfile = resolve(projectRoot, CONFIG_WELL_KNOWN_DIR, CONFIG_SCHEMA_FILE);
48041
+ if (schema == null) {
48042
+ await rm(outfile, { force: true });
48043
+ return {
48044
+ schema: null,
48045
+ warnings,
48046
+ written: false
48047
+ };
48048
+ }
48049
+ await mkdir(resolve(projectRoot, CONFIG_WELL_KNOWN_DIR), { recursive: true });
48050
+ await writeFile(outfile, JSON.stringify(schema), "utf8");
48051
+ return {
48052
+ schema,
48053
+ warnings,
48054
+ written: true
48055
+ };
48056
+ };
48057
+ var errMessage$2 = (err) => err instanceof Error ? err.message : String(err);
48058
+ /**
48059
+ * The default loader: when `config.schema.ts` exists at the project root, emit an
48060
+ * entry that re-exports ONLY its `configSchema` (static member access for
48061
+ * tree-shaking), esbuild-bundle it to
48062
+ * `<projectRoot>/.well-known/config/v1/config-schema.mjs`, import it back, and
48063
+ * read `configSchema` + `toJsonSchema`. When the file is ABSENT, return
48064
+ * `{ configSchema: null, toJsonSchema: () => null }` WITHOUT bundling — an absent
48065
+ * schema is silent, not a warning.
48066
+ *
48067
+ * Resolved FROM THE PROJECT: esbuild + zod come from the workspace's own
48068
+ * node_modules (like `buildWorkflows` resolves `@workflow/builders`), so the
48069
+ * bundle uses the SAME zod the schema was authored with and the heavy esbuild
48070
+ * tree stays out of the published CLI bundle.
48071
+ */
48072
+ var defaultConfigSchemaLoader = async (projectRoot) => {
48073
+ try {
48074
+ await access(resolve(projectRoot, CONFIG_SCHEMA_SOURCE));
48075
+ } catch {
48076
+ return {
48077
+ configSchema: null,
48078
+ toJsonSchema: () => null
48079
+ };
48080
+ }
48081
+ const projectRequire = createRequire(resolve(projectRoot, "package.json"));
48082
+ let esbuild;
48083
+ try {
48084
+ esbuild = await import(pathToFileURL(projectRequire.resolve("esbuild")).href);
48085
+ } catch (err) {
48086
+ throw new Error(`Cannot resolve esbuild from the workspace (needed to extract the config schema). Cause: ${errMessage$2(err)}`);
48087
+ }
48088
+ const entry = buildEntrySource$1();
48089
+ const outfile = resolve(projectRoot, CONFIG_WELL_KNOWN_DIR, CONFIG_SCHEMA_BUNDLE);
48090
+ await esbuild.build({
48091
+ stdin: {
48092
+ contents: entry,
48093
+ resolveDir: projectRoot,
48094
+ sourcefile: "config-schema-entry.ts",
48095
+ loader: "ts"
48096
+ },
48097
+ bundle: true,
48098
+ packages: "external",
48099
+ format: "esm",
48100
+ platform: "node",
48101
+ target: "node22",
48102
+ treeShaking: true,
48103
+ outfile,
48104
+ logLevel: "silent"
48105
+ });
48106
+ const mod = await import(`${pathToFileURL(outfile).href}?v=${Date.now()}`);
48107
+ if (typeof mod.toJsonSchema !== "function") throw new Error(`config-schema.mjs did not export the expected { configSchema, toJsonSchema } shape.`);
48108
+ return {
48109
+ configSchema: mod.configSchema ?? null,
48110
+ toJsonSchema: mod.toJsonSchema
48111
+ };
48112
+ };
48113
+ /**
48114
+ * Generate the bundle entry source. It NAMESPACE-imports `config.schema.ts` and
48115
+ * reads `configSchema` with DIRECT static member access — so esbuild tree-shakes
48116
+ * everything else and a MISSING export is simply `undefined` (→ `null`), never a
48117
+ * bundle-breaking "no matching export".
48118
+ *
48119
+ * The converter is REPLICATED here (not imported from the workflow extractor): it
48120
+ * runs inside the emitted `.mjs` with the PROJECT's own zod. It is byte-for-byte
48121
+ * the workflow extractor's converter MINUS the `half` parameter — config is a
48122
+ * single input-shaped object, so the Standard-Schema fallback invokes the
48123
+ * `.input` converter directly.
48124
+ */
48125
+ var buildEntrySource$1 = () => {
48126
+ return [
48127
+ `import { z } from 'zod';`,
48128
+ `import * as cfg from './config.schema.ts';`,
48129
+ `// Non-Zod Standard Schema fallback. Per @standard-schema/spec, ~standard`,
48130
+ `// .jsonSchema is a Converter { input(opts), output(opts) } of FUNCTIONS — so`,
48131
+ `// we INVOKE the input converter to get the actual JSON Schema (config is an`,
48132
+ `// input-shaped object). Returning the Converter object verbatim folds`,
48133
+ `// non-serialisable functions that JSON.stringify drops → a phantom {} schema.`,
48134
+ `const std = (s) => {`,
48135
+ ` const c = s && s['~standard'] && s['~standard'].jsonSchema;`,
48136
+ ` const fn = c && c.input;`,
48137
+ ` return typeof fn === 'function' ? fn({ target: 'draft-2020-12' }) : null;`,
48138
+ `};`,
48139
+ `const carriesFns = (o) =>`,
48140
+ ` o != null && typeof o === 'object' && Object.values(o).some((v) => typeof v === 'function');`,
48141
+ `export const toJsonSchema = (s) => {`,
48142
+ ` if (s == null) return null;`,
48143
+ ` let out;`,
48144
+ ` try { out = z.toJSONSchema(s); }`,
48145
+ ` catch (e) { out = std(s); if (out == null) throw e; }`,
48146
+ ` // A result carrying functions is a non-serialisable phantom; degrade to`,
48147
+ ` // null + a noisy warning instead of writing garbage to the artifact.`,
48148
+ ` if (carriesFns(out)) throw new Error('schema is non-serialisable (carries functions)');`,
48149
+ ` return out;`,
48150
+ `};`,
48151
+ `export const configSchema = cfg.configSchema ?? null;`
48152
+ ].join("\n");
48153
+ };
48154
+ //#endregion
47886
48155
  //#region src/dev/detect-workflow-streaming.ts
47887
48156
  /** `getWritable` imported from the literal `'workflow'` package (single or
47888
48157
  * double quotes, other named imports around it allowed). */
@@ -47952,7 +48221,7 @@ var extractWorkflowSchemas = async (projectRoot, workflows, opts) => {
47952
48221
  schemas[w.name] = { ...NULL_PAIR };
47953
48222
  schedules[w.name] = [];
47954
48223
  }
47955
- emit(`[workflow-schema] schema extraction bundle failed; no workflow input/output schemas are available — Playgrounds will fall back to a raw JSON field. Cause: ${errMessage$2(err)}`);
48224
+ emit(`[workflow-schema] schema extraction bundle failed; no workflow input/output schemas are available — Playgrounds will fall back to a raw JSON field. Cause: ${errMessage$1(err)}`);
47956
48225
  return {
47957
48226
  schemas,
47958
48227
  schedules,
@@ -48015,11 +48284,11 @@ var convertHalf = (workflowName, half, schema, toJsonSchema, emit) => {
48015
48284
  try {
48016
48285
  return toJsonSchema(schema, half);
48017
48286
  } catch (err) {
48018
- emit(`[workflow-schema] workflow "${workflowName}" declares an ${half}Schema that could not be converted to JSON Schema — the Playground will show a raw JSON field for it. Cause: ${errMessage$2(err)}`);
48287
+ emit(`[workflow-schema] workflow "${workflowName}" declares an ${half}Schema that could not be converted to JSON Schema — the Playground will show a raw JSON field for it. Cause: ${errMessage$1(err)}`);
48019
48288
  return null;
48020
48289
  }
48021
48290
  };
48022
- var errMessage$2 = (err) => err instanceof Error ? err.message : String(err);
48291
+ var errMessage$1 = (err) => err instanceof Error ? err.message : String(err);
48023
48292
  /**
48024
48293
  * The default loader: generate an entry that re-exports ONLY each workflow's
48025
48294
  * `inputSchema`/`outputSchema` (resolved from the workflow module or a
@@ -48041,9 +48310,9 @@ var defaultWorkflowSchemaLoader = async (projectRoot, workflows, outDir) => {
48041
48310
  try {
48042
48311
  esbuild = await import(pathToFileURL(projectRequire.resolve("esbuild")).href);
48043
48312
  } catch (err) {
48044
- throw new Error(`Cannot resolve esbuild from the workspace (needed to extract workflow schemas). Cause: ${errMessage$2(err)}`);
48313
+ throw new Error(`Cannot resolve esbuild from the workspace (needed to extract workflow schemas). Cause: ${errMessage$1(err)}`);
48045
48314
  }
48046
- const entry = await buildEntrySource$1(projectRoot, workflows);
48315
+ const entry = await buildEntrySource(projectRoot, workflows);
48047
48316
  const outfile = resolve(outDir, WORKFLOW_WELL_KNOWN_DIR, WORKFLOW_SCHEMAS_FILE);
48048
48317
  await esbuild.build({
48049
48318
  stdin: {
@@ -48078,7 +48347,7 @@ var defaultWorkflowSchemaLoader = async (projectRoot, workflows, outDir) => {
48078
48347
  * Schema source per workflow: a `<base>.contract.ts` sibling when it exists (the
48079
48348
  * clean variant — only zod, no closure), else the workflow module itself.
48080
48349
  */
48081
- var buildEntrySource$1 = async (projectRoot, workflows) => {
48350
+ var buildEntrySource = async (projectRoot, workflows) => {
48082
48351
  const lines = [
48083
48352
  `import { z } from 'zod';`,
48084
48353
  `// Non-Zod Standard Schema fallback. Per @standard-schema/spec, ~standard`,
@@ -48728,7 +48997,7 @@ var withEntry = (workflows, seed) => {
48728
48997
  export: seed.export
48729
48998
  }].sort((a, b) => a.name.localeCompare(b.name));
48730
48999
  };
48731
- var errMessage$1 = (err) => err instanceof Error ? err.message : String(err);
49000
+ var errMessage = (err) => err instanceof Error ? err.message : String(err);
48732
49001
  /**
48733
49002
  * Resolve both platform-managed workflows and return the workspace's workflow
48734
49003
  * list with them spliced in, ready for the compiler (which already knows how to
@@ -48744,14 +49013,14 @@ var seedManagedWorkflows = async (projectRoot, workflows, logger, deps = {}) =>
48744
49013
  logger.info(`rag-ingest workflow: using the ${seed.active === "default" ? "platform default" : "creator override"} (${seed.module})`);
48745
49014
  result = withEntry(result, seed);
48746
49015
  } catch (err) {
48747
- logger.warn({ err: errMessage$1(err) }, "failed to seed the default rag-ingest workflow — RAG upload ingest may be unavailable");
49016
+ logger.warn({ err: errMessage(err) }, "failed to seed the default rag-ingest workflow — RAG upload ingest may be unavailable");
48748
49017
  }
48749
49018
  try {
48750
49019
  const seed = await seedMappingSuggest(projectRoot);
48751
49020
  logger.info(`mapping-suggest workflow: using the ${seed.active === "default" ? "platform default" : "creator override"} (${seed.module})`);
48752
49021
  result = withEntry(result, seed);
48753
49022
  } catch (err) {
48754
- logger.warn({ err: errMessage$1(err) }, "failed to seed the default mapping-suggest workflow — Auto-map may be unavailable");
49023
+ logger.warn({ err: errMessage(err) }, "failed to seed the default mapping-suggest workflow — Auto-map may be unavailable");
48755
49024
  }
48756
49025
  return resolveSystemWorkflows(result);
48757
49026
  };
@@ -48988,6 +49257,7 @@ var buildWorkspace = async (ctx, outDirArg, deps = {}) => {
48988
49257
  const discover = deps.discoverWorkspace ?? discoverWorkspace;
48989
49258
  const build = deps.buildWorkspaceBundle ?? buildWorkspaceBundle;
48990
49259
  const seedManaged = deps.seedManagedWorkflows ?? seedManagedWorkflows;
49260
+ const extractSchema = deps.extractConfigSchema ?? extractConfigSchema;
48991
49261
  const projectRoot = ctx.cwd;
48992
49262
  const outDir = resolve(projectRoot, outDirArg ?? "dist/workspace-bundle");
48993
49263
  const workspace = await discover(projectRoot);
@@ -49010,6 +49280,7 @@ var buildWorkspace = async (ctx, outDirArg, deps = {}) => {
49010
49280
  workflows: workspace.workflows,
49011
49281
  logger: { warn: (msg) => ctx.logger.warn(msg) }
49012
49282
  }),
49283
+ resolveConfigSchema: () => extractSchema(projectRoot, { logger: { warn: (msg) => ctx.logger.warn(msg) } }).then((result) => result.schema),
49013
49284
  logger: {
49014
49285
  info: (msg) => ctx.logger.info(msg),
49015
49286
  warn: (msg) => ctx.logger.warn(msg)
@@ -49086,8 +49357,48 @@ var RUNTIME_PORT = 8080;
49086
49357
  var POSTGRES_IMAGE$1 = "pgvector/pgvector:pg17";
49087
49358
  /** Redis image the generated compose runs (BullMQ backs the durable World). */
49088
49359
  var REDIS_IMAGE$1 = "redis:7-alpine";
49360
+ /**
49361
+ * S3-compatible object storage the generated compose runs.
49362
+ *
49363
+ * The compose already ships a pgvector Postgres BECAUSE of managed RAG — and
49364
+ * managed RAG stores the uploaded ORIGINAL in object storage before it chunks
49365
+ * and embeds it (`putManagedObject`). Shipping the vector database without a
49366
+ * bucket delivers half of that feature: ingestion fails at the first upload,
49367
+ * `client.storage` returns `s3_credentials_missing`, and Studio's file browser
49368
+ * has nothing to talk to — while `/health` still answers `ok`, because it only
49369
+ * probes Postgres and Redis.
49370
+ *
49371
+ * Same image and pin as `stackbone dev`'s compose, so the emulator and the
49372
+ * deliverable run the identical backend.
49373
+ */
49374
+ var MINIO_IMAGE$1 = "minio/minio:RELEASE.2025-03-12T18-04-18Z";
49375
+ /** Access key the bundled MinIO is created with. The secret is generated per package. */
49376
+ var MINIO_ACCESS_KEY = "stackbone";
49377
+ /** Bucket the init container creates, and the agent's `STACKBONE_S3_BUCKET`. */
49378
+ var MINIO_BUCKET = "stackbone";
49379
+ /**
49380
+ * Host port the bundled MinIO's S3 API is published on.
49381
+ *
49382
+ * Published, unlike Postgres and Redis, and that is deliberate: Studio's file
49383
+ * browser hands the operator's BROWSER a presigned download URL, and a presigned
49384
+ * URL carries the endpoint it was signed with. Signed against the compose-internal
49385
+ * `http://minio:9000`, the link resolves only inside the compose network. The
49386
+ * README explains the one-line fix (point `STACKBONE_S3_ENDPOINT` at a host both
49387
+ * the container and the browser can reach) — publishing the port is what makes
49388
+ * that fix possible at all.
49389
+ */
49390
+ var MINIO_PORT = 9e3;
49089
49391
  /** A fresh 32-byte key, base64 — the format `STACKBONE_SECRET_KEY` expects. */
49090
49392
  var generateSecretKey = () => randomBytes(32).toString("base64");
49393
+ /**
49394
+ * A fresh root password for the bundled MinIO.
49395
+ *
49396
+ * `base64url`, not `base64`: this value travels through a `.env` line, a compose
49397
+ * interpolation and a `/bin/sh -c` entrypoint, and `+` `/` `=` survive that trip
49398
+ * badly. Generated per package rather than hardcoded because, unlike Postgres and
49399
+ * Redis, this service publishes a port on the host.
49400
+ */
49401
+ var generateStoragePassword = () => randomBytes(24).toString("base64url");
49091
49402
  var renderDockerfile = (input) => `# Generated by \`stackbone package\`. Two lines is the whole build: the Stackbone
49092
49403
  # runtime base already carries the server and its pinned dependencies, so this
49093
49404
  # image adds only your workspace. No monorepo, no npm install, seconds to build.
@@ -49096,10 +49407,10 @@ COPY ${input.workspaceDirName}/ /app/workspace/
49096
49407
  `;
49097
49408
  var renderCompose = (input) => {
49098
49409
  const image = input.imageTag;
49099
- return `# Generated by \`stackbone package\`. Brings up the agent with a Postgres and a
49100
- # Redis of its own. If you already run those, delete both services here and point
49101
- # DATABASE_URL / WORKFLOW_REDIS_URL in .env at yours — read the README first, the
49102
- # database has one requirement.
49410
+ return `# Generated by \`stackbone package\`. Brings up the agent with a Postgres, a
49411
+ # Redis and an object store of its own. If you already run those, delete the
49412
+ # services here and point DATABASE_URL / WORKFLOW_REDIS_URL / STACKBONE_S3_* in
49413
+ # .env at yours — read the README first, the database has one requirement.
49103
49414
 
49104
49415
  services:
49105
49416
  postgres:
@@ -49131,6 +49442,45 @@ services:
49131
49442
  retries: 20
49132
49443
  restart: unless-stopped
49133
49444
 
49445
+ minio:
49446
+ # S3-compatible object storage: the agent's own files (\`client.storage\`), the
49447
+ # originals managed RAG ingests, and what Studio's file browser lists.
49448
+ image: ${MINIO_IMAGE$1}
49449
+ command: ['server', '/data']
49450
+ environment:
49451
+ MINIO_ROOT_USER: ${MINIO_ACCESS_KEY}
49452
+ MINIO_ROOT_PASSWORD: \${MINIO_ROOT_PASSWORD:?set MINIO_ROOT_PASSWORD in .env}
49453
+ ports:
49454
+ # Published so a presigned download link can be opened from a browser —
49455
+ # see "Downloads from the file browser" in the README.
49456
+ - '${MINIO_PORT}:9000'
49457
+ volumes:
49458
+ - agent-objects:/data
49459
+ healthcheck:
49460
+ test: ['CMD', 'mc', 'ready', 'local']
49461
+ interval: 5s
49462
+ timeout: 5s
49463
+ retries: 20
49464
+ restart: unless-stopped
49465
+
49466
+ minio-create-bucket:
49467
+ # Creates the bucket once, then exits 0. \`mc mb -p\` is idempotent, so it is a
49468
+ # no-op on every later start. The agent waits for it to SUCCEED rather than
49469
+ # for MinIO to be healthy: a bucket that does not exist yet fails the agent's
49470
+ # first upload with a NoSuchBucket nobody would connect back to this file.
49471
+ image: ${MINIO_IMAGE$1}
49472
+ restart: 'no'
49473
+ depends_on:
49474
+ minio:
49475
+ condition: service_healthy
49476
+ entrypoint: >
49477
+ /bin/sh -c "
49478
+ mc alias set local http://minio:9000 ${MINIO_ACCESS_KEY} \\"$$MINIO_ROOT_PASSWORD\\" &&
49479
+ mc mb -p local/${MINIO_BUCKET}
49480
+ "
49481
+ environment:
49482
+ MINIO_ROOT_PASSWORD: \${MINIO_ROOT_PASSWORD:?set MINIO_ROOT_PASSWORD in .env}
49483
+
49134
49484
  agent:
49135
49485
  ${input.imageArchiveName !== void 0 ? ` # Loaded from ${input.imageArchiveName} — there is nothing to build here.
49136
49486
  image: ${image}` : ` build:
@@ -49140,9 +49490,17 @@ ${input.imageArchiveName !== void 0 ? ` # Loaded from ${input.imageArchiveNam
49140
49490
  env_file:
49141
49491
  - .env
49142
49492
  environment:
49143
- # Point the agent at the two services above. Override in .env to use your own.
49493
+ # Point the agent at the three services above. Override in .env to use your own.
49144
49494
  DATABASE_URL: \${DATABASE_URL:-postgres://stackbone:\${POSTGRES_PASSWORD:-stackbone}@postgres:5432/stackbone}
49145
49495
  WORKFLOW_REDIS_URL: \${WORKFLOW_REDIS_URL:-redis://redis:6379}
49496
+ STACKBONE_S3_ENDPOINT: \${STACKBONE_S3_ENDPOINT:-http://minio:9000}
49497
+ STACKBONE_S3_ACCESS_KEY: \${STACKBONE_S3_ACCESS_KEY:-${MINIO_ACCESS_KEY}}
49498
+ STACKBONE_S3_SECRET_KEY: \${STACKBONE_S3_SECRET_KEY:-\${MINIO_ROOT_PASSWORD}}
49499
+ STACKBONE_S3_BUCKET: \${STACKBONE_S3_BUCKET:-${MINIO_BUCKET}}
49500
+ STACKBONE_S3_REGION: \${STACKBONE_S3_REGION:-us-east-1}
49501
+ # MinIO addresses buckets by path. A managed bucket that only answers to the
49502
+ # bucket-as-subdomain form (AWS S3, Railway Buckets) needs this set to false.
49503
+ STACKBONE_S3_FORCE_PATH_STYLE: \${STACKBONE_S3_FORCE_PATH_STYLE:-true}
49146
49504
  ports:
49147
49505
  # The agent serves on ${RUNTIME_PORT} inside the container. Change the LEFT
49148
49506
  # number to publish it elsewhere on the host.
@@ -49152,25 +49510,51 @@ ${input.imageArchiveName !== void 0 ? ` # Loaded from ${input.imageArchiveNam
49152
49510
  condition: service_healthy
49153
49511
  redis:
49154
49512
  condition: service_healthy
49513
+ minio-create-bucket:
49514
+ condition: service_completed_successfully
49155
49515
  restart: unless-stopped
49156
49516
 
49157
49517
  volumes:
49158
49518
  agent-postgres:
49159
49519
  agent-redis:
49520
+ agent-objects:
49160
49521
  `;
49161
49522
  };
49162
- var renderEnvFile = (input, secretKey) => `# Generated by \`stackbone package\` for ${input.agentName}.
49523
+ /**
49524
+ * The `.env` and the `.env.example` are ONE template rendered twice.
49525
+ *
49526
+ * They differ only in the two secret lines: the real file carries freshly minted
49527
+ * values so `docker compose up -d` works untouched, the example carries
49528
+ * placeholders so it can be committed next to the folder. Keeping them as one
49529
+ * string is what stops the committed template from drifting into a stale list of
49530
+ * variables the runtime no longer reads.
49531
+ */
49532
+ var renderEnv = (input, secrets) => {
49533
+ const generated = secrets !== null;
49534
+ return `${generated ? `# Generated by \`stackbone package\` for ${input.agentName}.
49163
49535
  #
49164
- # Bringing this up with the bundled Postgres and Redis needs NOTHING else filled
49165
- # in — the key below is already generated and the two URLs default to the compose
49166
- # services. Set them explicitly only to use your own infrastructure.
49536
+ # Bringing this up with the bundled Postgres, Redis and MinIO needs NOTHING else
49537
+ # filled in — the secrets below are already generated and the endpoints default
49538
+ # to the compose services. Set them explicitly only to use your own infrastructure.` : `# Template for the \`.env\` \`stackbone package\` generates for ${input.agentName}.
49539
+ #
49540
+ # The packaged folder already contains a working \`.env\` with both secrets minted.
49541
+ # This file is the committable copy: it documents every variable the agent reads
49542
+ # and carries NO secret values. Filling it in by hand is only needed when you
49543
+ # rebuild the folder somewhere the original \`.env\` did not travel.`}
49167
49544
 
49168
49545
  # ---- Keep this safe ----------------------------------------------------------
49169
49546
  # Every secret this agent stores in its database is encrypted with this key. It
49170
49547
  # cannot live in the database it protects. LOSE IT AND YOU LOSE THOSE SECRETS —
49171
49548
  # there is no recovery, they have to be entered again. Changing it has the same
49172
49549
  # effect as losing it.
49173
- STACKBONE_SECRET_KEY=${secretKey}
49550
+ ${generated ? `STACKBONE_SECRET_KEY=${secrets.secretKey}` : `# Generate with: openssl rand -base64 32
49551
+ STACKBONE_SECRET_KEY=`}
49552
+
49553
+ # Root password of the bundled object store, which also serves as the agent's
49554
+ # STACKBONE_S3_SECRET_KEY. Unlike the database and Redis, this service publishes
49555
+ # a port on the host — leave this value strong, or do not publish the port.
49556
+ ${generated ? `MINIO_ROOT_PASSWORD=${secrets.storagePassword}` : `# Generate with: openssl rand -base64 24 | tr '+/' '-_' | tr -d '='
49557
+ MINIO_ROOT_PASSWORD=`}
49174
49558
 
49175
49559
  # ---- Your infrastructure (optional here) -------------------------------------
49176
49560
  # Uncomment to point the agent at a Postgres and a Redis you already run, instead
@@ -49182,19 +49566,47 @@ STACKBONE_SECRET_KEY=${secretKey}
49182
49566
  # Managed Redis always wants a password, and some require TLS — use rediss:// for
49183
49567
  # those. Percent-encode a password containing / + = or @, or the URL will not parse.
49184
49568
 
49569
+ # ---- Object storage ----------------------------------------------------------
49570
+ # The agent stores its own files here (\`client.storage\`), and managed RAG keeps
49571
+ # every ingested original here before it chunks and embeds it. Left alone, these
49572
+ # point at the MinIO this compose file starts.
49573
+ #
49574
+ # Uncomment to use a managed bucket instead (AWS S3, Cloudflare R2, Azure Blob,
49575
+ # Google Cloud Storage, Railway Buckets — anything S3-compatible). Create the
49576
+ # bucket first; the agent never creates one.
49577
+ # STACKBONE_S3_ENDPOINT=https://s3.eu-west-1.amazonaws.com
49578
+ # STACKBONE_S3_ACCESS_KEY=
49579
+ # STACKBONE_S3_SECRET_KEY=
49580
+ # STACKBONE_S3_BUCKET=
49581
+ # STACKBONE_S3_REGION=eu-west-1
49582
+ #
49583
+ # Addressing style. \`true\` puts the bucket in the PATH
49584
+ # (\`https://host/bucket/key\`), which is what MinIO and Cloudflare R2 accept.
49585
+ # Set it to \`false\` for a bucket that only answers to the SUBDOMAIN form
49586
+ # (\`https://bucket.host/key\`) — AWS S3 and Railway Buckets are the common ones.
49587
+ # Symptom of the wrong value: every call fails with NoSuchBucket or a 403 even
49588
+ # though the credentials are right.
49589
+ # STACKBONE_S3_FORCE_PATH_STYLE=true
49590
+
49185
49591
  # ---- Optional ----------------------------------------------------------------
49186
49592
  # Set if your database role is not allowed to create schemas; you then apply the
49187
49593
  # SQL shipped inside the image by hand before the first boot. The README explains
49188
49594
  # where it lives.
49189
49595
  # STACKBONE_SKIP_MIGRATIONS=1
49190
49596
  `;
49597
+ };
49598
+ var renderEnvFile = (input, secrets) => renderEnv(input, secrets);
49599
+ /** The committable, secret-free twin of the generated `.env`. */
49600
+ var renderEnvExample = (input) => renderEnv(input, null);
49191
49601
  var renderInventory = (label, names) => names.length === 0 ? `No ${label}.` : names.map((n) => `- \`${n}\``).join("\n");
49192
49602
  var renderReadme = (input) => {
49193
49603
  const offline = input.imageArchiveName;
49194
49604
  return `# ${input.agentName}
49195
49605
 
49196
- A Stackbone agent, packaged to run in your own environment. It talks to a Postgres
49197
- and a Redis that you control, and needs no connection to Stackbone to run.
49606
+ A Stackbone agent, packaged to run in your own environment. It talks to a Postgres,
49607
+ a Redis and an object store that you control, and needs no connection to Stackbone
49608
+ to run. This folder starts all three for you; swapping in your own is a few lines
49609
+ in \`.env\`.
49198
49610
 
49199
49611
  ## What you are running
49200
49612
 
@@ -49235,10 +49647,36 @@ prints it again. You need that value only if you later connect this deployment t
49235
49647
  a Stackbone control plane. Copy it somewhere safe now if there is any chance you
49236
49648
  will. Anyone who can read these logs can read it too.
49237
49649
 
49238
- ## Using your own Postgres and Redis
49650
+ ## Downloads from the file browser
49651
+
49652
+ Everything the agent itself does with files works out of the box. One thing does
49653
+ not, and it is worth thirty seconds of your time now rather than a puzzled bug
49654
+ report later.
49655
+
49656
+ When Studio's file browser offers you a download, it hands your BROWSER a signed,
49657
+ time-limited link. That link points at whatever address the agent knows the object
49658
+ store by — and by default that address is \`http://minio:9000\`, a name that only
49659
+ exists inside this compose network. Your browser cannot resolve it, so the
49660
+ download fails while every upload, every RAG ingestion and every listing works.
49661
+
49662
+ If anyone will download files from the file browser, set \`STACKBONE_S3_ENDPOINT\`
49663
+ in \`.env\` to an address that BOTH the container and the browser can reach — the
49664
+ host's own name or IP, on the published port:
49665
+
49666
+ \`\`\`bash
49667
+ STACKBONE_S3_ENDPOINT=http://the-host-you-run-this-on:${MINIO_PORT}
49668
+ \`\`\`
49669
+
49670
+ Port \`${MINIO_PORT}\` is published for exactly this. If nobody needs those downloads,
49671
+ delete the \`ports:\` block from the \`minio\` service and keep the object store off
49672
+ the network entirely.
49673
+
49674
+ ## Using your own Postgres, Redis and bucket
49239
49675
 
49240
- Delete the \`postgres\` and \`redis\` services from \`docker-compose.yml\`, then set
49241
- \`DATABASE_URL\` and \`WORKFLOW_REDIS_URL\` in \`.env\`.
49676
+ Delete the \`postgres\`, \`redis\`, \`minio\` and \`minio-create-bucket\` services from
49677
+ \`docker-compose.yml\` (and the \`depends_on\` entries naming them), then set
49678
+ \`DATABASE_URL\`, \`WORKFLOW_REDIS_URL\` and the \`STACKBONE_S3_*\` variables in \`.env\`.
49679
+ Do them one at a time if you prefer — the four services are independent.
49242
49680
 
49243
49681
  **The database has one requirement: the \`pgvector\` extension must be available.**
49244
49682
  The agent runs \`CREATE EXTENSION IF NOT EXISTS vector\` while creating its schema.
@@ -49273,6 +49711,24 @@ agent PINGs Redis before it starts and stops with the reason if it cannot: a
49273
49711
  rejected password fails right away, and a Redis still starting up is retried for
49274
49712
  30 seconds first.
49275
49713
 
49714
+ For the bucket, any S3-compatible storage works — AWS S3, Cloudflare R2, Azure
49715
+ Blob, Google Cloud Storage, Railway Buckets, another MinIO. **Create the bucket
49716
+ first**; the agent never creates one. Then fill in the five \`STACKBONE_S3_*\`
49717
+ variables, and check the sixth:
49718
+
49719
+ \`\`\`bash
49720
+ STACKBONE_S3_FORCE_PATH_STYLE=true # bucket in the path: https://host/bucket/key
49721
+ STACKBONE_S3_FORCE_PATH_STYLE=false # bucket as a subdomain: https://bucket.host/key
49722
+ \`\`\`
49723
+
49724
+ MinIO and Cloudflare R2 take the first. AWS S3 and Railway Buckets want the
49725
+ second. Get it wrong and every call fails with \`NoSuchBucket\` or a bare 403 while
49726
+ the credentials are perfectly valid — which is a genuinely confusing hour, so it
49727
+ is worth checking before you conclude the keys are wrong.
49728
+
49729
+ The agent writes only under a prefix of its own inside that bucket, so a bucket
49730
+ shared with something else stays tidy.
49731
+
49276
49732
  ## Running it somewhere other than Compose
49277
49733
 
49278
49734
  The agent serves on port **${RUNTIME_PORT}** by default, and binds whatever
@@ -49364,6 +49820,7 @@ var runPackage = async (ctx, args, deps = {}) => {
49364
49820
  const build = deps.buildWorkspace ?? buildWorkspace;
49365
49821
  const docker = deps.runDocker ?? runDockerCommand;
49366
49822
  const makeKey = deps.generateSecretKey ?? generateSecretKey;
49823
+ const makeStoragePassword = deps.generateStoragePassword ?? generateStoragePassword;
49367
49824
  const projectRoot = ctx.cwd;
49368
49825
  const outDir = resolve(projectRoot, args["out-dir"] ?? "dist/deploy");
49369
49826
  const { bundle } = await build(ctx, void 0, deps);
@@ -49383,16 +49840,21 @@ var runPackage = async (ctx, args, deps = {}) => {
49383
49840
  recursive: true,
49384
49841
  force: true
49385
49842
  });
49386
- const secretKey = makeKey();
49843
+ const secrets = {
49844
+ secretKey: makeKey(),
49845
+ storagePassword: makeStoragePassword()
49846
+ };
49387
49847
  const written = [
49388
49848
  "Dockerfile",
49389
49849
  "docker-compose.yml",
49390
49850
  ".env",
49851
+ ".env.example",
49391
49852
  "README.md"
49392
49853
  ];
49393
49854
  await writeFile(join(outDir, "Dockerfile"), renderDockerfile(templateInput), "utf8");
49394
49855
  await writeFile(join(outDir, "docker-compose.yml"), renderCompose(templateInput), "utf8");
49395
- await writeFile(join(outDir, ".env"), renderEnvFile(templateInput, secretKey), "utf8");
49856
+ await writeFile(join(outDir, ".env"), renderEnvFile(templateInput, secrets), "utf8");
49857
+ await writeFile(join(outDir, ".env.example"), renderEnvExample(templateInput), "utf8");
49396
49858
  await writeFile(join(outDir, "README.md"), renderReadme(templateInput), "utf8");
49397
49859
  if (args.offline) {
49398
49860
  ctx.logger.info(`Building ${imageTag} and saving it to ${IMAGE_ARCHIVE_NAME} (~150 MB)`);
@@ -49421,9 +49883,11 @@ var runPackage = async (ctx, args, deps = {}) => {
49421
49883
  ...degraded.length > 0 ? [` degraded ${degraded.map((d) => d.name).join(", ")} — shipped, but they will not run`] : [],
49422
49884
  ` serves on ${RUNTIME_PORT} (or PORT, when the host assigns one)`,
49423
49885
  "",
49424
- "The .env holds a freshly generated STACKBONE_SECRET_KEY. Everything this",
49425
- "agent stores is encrypted with it and it cannot be recovered — hand the",
49426
- "folder over intact, and keep a copy of that key somewhere safe.",
49886
+ "The .env holds two freshly generated secrets: STACKBONE_SECRET_KEY, which",
49887
+ "encrypts everything this agent stores and cannot be recovered, and",
49888
+ "MINIO_ROOT_PASSWORD for the bundled object store. Hand the folder over",
49889
+ "intact, and keep a copy of that key somewhere safe. The .env.example next",
49890
+ "to it carries the same variables with no values, for committing.",
49427
49891
  ...args.offline ? [] : [
49428
49892
  "",
49429
49893
  `Whoever runs this needs to reach ${templateInput.baseImage} once, to pull it.`,
@@ -49469,181 +49933,6 @@ var createPackageCommand = defineVerb({
49469
49933
  handler: ({ ctx, args }) => runPackage(ctx, args)
49470
49934
  });
49471
49935
  //#endregion
49472
- //#region src/dev/extract-config-schema.ts
49473
- /** Directory the extracted artifact lives in, relative to the workspace root.
49474
- * A subdir of its own, parallel to the workflow well-known tree. */
49475
- var CONFIG_WELL_KNOWN_DIR = ".well-known/config/v1";
49476
- /** The shipped artifact: the single JSON Schema (JSON only, no live sidecar). */
49477
- var CONFIG_SCHEMA_FILE = "config-schema.json";
49478
- /** The intermediate esbuild bundle (harmless to leave behind, like the workflow
49479
- * sidecar). NOT the deliverable — `config-schema.json` is. */
49480
- var CONFIG_SCHEMA_BUNDLE = "config-schema.mjs";
49481
- /**
49482
- * Extract the workspace's single config JSON Schema. Defensive by contract: an
49483
- * absent `config.schema.ts` degrades to `null` SILENTLY (the editor stays
49484
- * free-form); a DECLARED-but-inconvertible schema degrades to `null` plus one
49485
- * noisy warning; and a total bundle failure degrades to `null` plus one coarse
49486
- * warning. It NEVER throws — a broken config schema must not break the dev build.
49487
- */
49488
- var extractConfigSchema = async (projectRoot, opts = {}) => {
49489
- const loader = opts.loader ?? defaultConfigSchemaLoader;
49490
- const warnings = [];
49491
- const emit = (msg) => {
49492
- warnings.push(msg);
49493
- opts.logger?.warn(msg);
49494
- };
49495
- let loaded;
49496
- try {
49497
- loaded = await loader(projectRoot);
49498
- } catch (err) {
49499
- emit(`[config-schema] config schema extraction bundle failed; no config schema is available — the Config editor will fall back to a raw JSON field. Cause: ${errMessage(err)}`);
49500
- return {
49501
- schema: null,
49502
- warnings
49503
- };
49504
- }
49505
- if (loaded.configSchema == null) return {
49506
- schema: null,
49507
- warnings
49508
- };
49509
- try {
49510
- return {
49511
- schema: loaded.toJsonSchema(loaded.configSchema),
49512
- warnings
49513
- };
49514
- } catch (err) {
49515
- emit(`[config-schema] config.schema.ts could not be converted to JSON Schema — the Config editor will fall back to a raw JSON field. Cause: ${errMessage(err)}`);
49516
- return {
49517
- schema: null,
49518
- warnings
49519
- };
49520
- }
49521
- };
49522
- /**
49523
- * Extract the config schema and WRITE the artifact. When the schema is non-null,
49524
- * writes `<projectRoot>/.well-known/config/v1/config-schema.json` (compact JSON,
49525
- * like `manifest.json`) and returns `written: true`. When the schema is null
49526
- * (absent or inconvertible), REMOVES any stale `config-schema.json` so a deleted
49527
- * or broken schema does not leave a phantom artifact, and returns `written:
49528
- * false`. Never throws (mirrors `extractConfigSchema`).
49529
- */
49530
- var buildConfigSchema = async (projectRoot, opts = {}) => {
49531
- const { schema, warnings } = await extractConfigSchema(projectRoot, opts);
49532
- const outfile = resolve(projectRoot, CONFIG_WELL_KNOWN_DIR, CONFIG_SCHEMA_FILE);
49533
- if (schema == null) {
49534
- await rm(outfile, { force: true });
49535
- return {
49536
- schema: null,
49537
- warnings,
49538
- written: false
49539
- };
49540
- }
49541
- await mkdir(resolve(projectRoot, CONFIG_WELL_KNOWN_DIR), { recursive: true });
49542
- await writeFile(outfile, JSON.stringify(schema), "utf8");
49543
- return {
49544
- schema,
49545
- warnings,
49546
- written: true
49547
- };
49548
- };
49549
- var errMessage = (err) => err instanceof Error ? err.message : String(err);
49550
- /**
49551
- * The default loader: when `config.schema.ts` exists at the project root, emit an
49552
- * entry that re-exports ONLY its `configSchema` (static member access for
49553
- * tree-shaking), esbuild-bundle it to
49554
- * `<projectRoot>/.well-known/config/v1/config-schema.mjs`, import it back, and
49555
- * read `configSchema` + `toJsonSchema`. When the file is ABSENT, return
49556
- * `{ configSchema: null, toJsonSchema: () => null }` WITHOUT bundling — an absent
49557
- * schema is silent, not a warning.
49558
- *
49559
- * Resolved FROM THE PROJECT: esbuild + zod come from the workspace's own
49560
- * node_modules (like `buildWorkflows` resolves `@workflow/builders`), so the
49561
- * bundle uses the SAME zod the schema was authored with and the heavy esbuild
49562
- * tree stays out of the published CLI bundle.
49563
- */
49564
- var defaultConfigSchemaLoader = async (projectRoot) => {
49565
- try {
49566
- await access(resolve(projectRoot, CONFIG_SCHEMA_SOURCE));
49567
- } catch {
49568
- return {
49569
- configSchema: null,
49570
- toJsonSchema: () => null
49571
- };
49572
- }
49573
- const projectRequire = createRequire(resolve(projectRoot, "package.json"));
49574
- let esbuild;
49575
- try {
49576
- esbuild = await import(pathToFileURL(projectRequire.resolve("esbuild")).href);
49577
- } catch (err) {
49578
- throw new Error(`Cannot resolve esbuild from the workspace (needed to extract the config schema). Cause: ${errMessage(err)}`);
49579
- }
49580
- const entry = buildEntrySource();
49581
- const outfile = resolve(projectRoot, CONFIG_WELL_KNOWN_DIR, CONFIG_SCHEMA_BUNDLE);
49582
- await esbuild.build({
49583
- stdin: {
49584
- contents: entry,
49585
- resolveDir: projectRoot,
49586
- sourcefile: "config-schema-entry.ts",
49587
- loader: "ts"
49588
- },
49589
- bundle: true,
49590
- packages: "external",
49591
- format: "esm",
49592
- platform: "node",
49593
- target: "node22",
49594
- treeShaking: true,
49595
- outfile,
49596
- logLevel: "silent"
49597
- });
49598
- const mod = await import(`${pathToFileURL(outfile).href}?v=${Date.now()}`);
49599
- if (typeof mod.toJsonSchema !== "function") throw new Error(`config-schema.mjs did not export the expected { configSchema, toJsonSchema } shape.`);
49600
- return {
49601
- configSchema: mod.configSchema ?? null,
49602
- toJsonSchema: mod.toJsonSchema
49603
- };
49604
- };
49605
- /**
49606
- * Generate the bundle entry source. It NAMESPACE-imports `config.schema.ts` and
49607
- * reads `configSchema` with DIRECT static member access — so esbuild tree-shakes
49608
- * everything else and a MISSING export is simply `undefined` (→ `null`), never a
49609
- * bundle-breaking "no matching export".
49610
- *
49611
- * The converter is REPLICATED here (not imported from the workflow extractor): it
49612
- * runs inside the emitted `.mjs` with the PROJECT's own zod. It is byte-for-byte
49613
- * the workflow extractor's converter MINUS the `half` parameter — config is a
49614
- * single input-shaped object, so the Standard-Schema fallback invokes the
49615
- * `.input` converter directly.
49616
- */
49617
- var buildEntrySource = () => {
49618
- return [
49619
- `import { z } from 'zod';`,
49620
- `import * as cfg from './config.schema.ts';`,
49621
- `// Non-Zod Standard Schema fallback. Per @standard-schema/spec, ~standard`,
49622
- `// .jsonSchema is a Converter { input(opts), output(opts) } of FUNCTIONS — so`,
49623
- `// we INVOKE the input converter to get the actual JSON Schema (config is an`,
49624
- `// input-shaped object). Returning the Converter object verbatim folds`,
49625
- `// non-serialisable functions that JSON.stringify drops → a phantom {} schema.`,
49626
- `const std = (s) => {`,
49627
- ` const c = s && s['~standard'] && s['~standard'].jsonSchema;`,
49628
- ` const fn = c && c.input;`,
49629
- ` return typeof fn === 'function' ? fn({ target: 'draft-2020-12' }) : null;`,
49630
- `};`,
49631
- `const carriesFns = (o) =>`,
49632
- ` o != null && typeof o === 'object' && Object.values(o).some((v) => typeof v === 'function');`,
49633
- `export const toJsonSchema = (s) => {`,
49634
- ` if (s == null) return null;`,
49635
- ` let out;`,
49636
- ` try { out = z.toJSONSchema(s); }`,
49637
- ` catch (e) { out = std(s); if (out == null) throw e; }`,
49638
- ` // A result carrying functions is a non-serialisable phantom; degrade to`,
49639
- ` // null + a noisy warning instead of writing garbage to the artifact.`,
49640
- ` if (carriesFns(out)) throw new Error('schema is non-serialisable (carries functions)');`,
49641
- ` return out;`,
49642
- `};`,
49643
- `export const configSchema = cfg.configSchema ?? null;`
49644
- ].join("\n");
49645
- };
49646
- //#endregion
49647
49936
  //#region src/dev/generate-config-types.ts
49648
49937
  /** Path of the generated artifact relative to the agent root. */
49649
49938
  var CONFIG_TYPES_RELATIVE_PATH = join(".stackbone", "config.d.ts");
@@ -55415,9 +55704,10 @@ new version means deploying a new image and re-running \`link\`.
55415
55704
 
55416
55705
  Turn this project into a folder someone else can run, with no access to
55417
55706
  Stackbone and no knowledge of it. The folder holds your compiled workspace and
55418
- four generated files: a two-line \`Dockerfile\`, a \`docker-compose.yml\` that also
55419
- brings up a Postgres and a Redis, an \`.env\` carrying a freshly generated
55420
- \`STACKBONE_SECRET_KEY\`, and a \`README.md\` written for whoever receives it.
55707
+ five generated files: a two-line \`Dockerfile\`, a \`docker-compose.yml\` that also
55708
+ brings up a Postgres, a Redis and an object store, an \`.env\` carrying freshly
55709
+ generated secrets, an \`.env.example\` you can commit, and a \`README.md\` written
55710
+ for whoever receives it.
55421
55711
 
55422
55712
  \`\`\`sh
55423
55713
  stackbone package # writes dist/deploy
@@ -55452,9 +55742,10 @@ What lands in the output directory:
55452
55742
  dist/deploy/
55453
55743
  workspace/ # the compiled bundle, exactly what \`stackbone build\` writes
55454
55744
  Dockerfile # FROM the published runtime base, COPY workspace/
55455
- docker-compose.yml # the agent, a Postgres with pgvector, a Redis
55456
- .env # a generated STACKBONE_SECRET_KEY; nothing you must fill in
55457
- README.md # how to start it, use your own database, run it without Compose
55745
+ docker-compose.yml # the agent, a Postgres with pgvector, a Redis, a MinIO
55746
+ .env # generated secrets; nothing you must fill in
55747
+ .env.example # the same variables with no values, safe to commit
55748
+ README.md # how to start it, use your own services, run it without Compose
55458
55749
  agent-image.tar # only with --offline
55459
55750
  \`\`\`
55460
55751
 
@@ -55471,8 +55762,9 @@ to build. That variant needs Docker installed and running where you package, and
55471
55762
  it emits no \`build:\` block in the compose file: building is the thing the flag
55472
55763
  exists to avoid.
55473
55764
 
55474
- **Two things to tell whoever runs it.** Both are already in the generated
55475
- README, and both fail in a way that reads like a bug in your agent.
55765
+ **Three things to tell whoever runs it.** All three are already in the
55766
+ generated README, and all three fail in a way that reads like a bug in your
55767
+ agent.
55476
55768
 
55477
55769
  The database needs the **\`pgvector\` extension available**. The first boot runs
55478
55770
  \`CREATE EXTENSION IF NOT EXISTS vector\` while creating its schema, so a stock
@@ -55482,6 +55774,16 @@ Postgres applies most of the schema and then stops with
55482
55774
  of your own and the extension has to be available there, which on many managed
55483
55775
  databases only an administrator can create.
55484
55776
 
55777
+ **Downloads from the dashboard's file browser need a reachable endpoint.** The
55778
+ bundled MinIO answers to \`http://minio:9000\`, a name that only exists inside the
55779
+ Compose network. Everything the agent itself does with files works with that
55780
+ address, and so does RAG ingestion. A download link is the exception: the
55781
+ dashboard signs it and hands it to a **browser**, which cannot resolve that
55782
+ name. If anyone will download files, set \`STACKBONE_S3_ENDPOINT\` in \`.env\` to an
55783
+ address both the container and the browser reach, such as
55784
+ \`http://the-host-you-run-this-on:9000\`. Port \`9000\` is published for exactly
55785
+ that.
55786
+
55485
55787
  **The agent serves on port 8080, or on \`PORT\` when the host sets one.** Hosts
55486
55788
  that assign a port and expect the application to adopt it (Cloud Run, Railway,
55487
55789
  Heroku, App Runner) need no extra configuration. Point their health check at
@@ -55489,9 +55791,23 @@ Heroku, App Runner) need no extra configuration. Point their health check at
55489
55791
  while \`/health\` also probes the database, which the very first boot is still
55490
55792
  creating.
55491
55793
 
55794
+ **Using a managed bucket instead.** Any S3-compatible storage works (AWS S3,
55795
+ Cloudflare R2, Azure Blob, Google Cloud Storage, another MinIO). Create the
55796
+ bucket first, because the agent never creates one, then fill in the
55797
+ \`STACKBONE_S3_*\` variables in \`.env\`. One of them decides how the bucket is
55798
+ addressed: \`STACKBONE_S3_FORCE_PATH_STYLE=true\` puts the bucket in the path
55799
+ (\`https://host/bucket/key\`), which MinIO and Cloudflare R2 accept, and \`false\`
55800
+ puts it in a subdomain (\`https://bucket.host/key\`), which AWS S3 expects. The
55801
+ wrong value fails every call with \`NoSuchBucket\` or a bare 403 while the
55802
+ credentials are valid, so check it before you suspect the keys.
55803
+
55492
55804
  **Hand the folder over intact.** The generated \`.env\` holds the only copy of
55493
55805
  \`STACKBONE_SECRET_KEY\`, and everything the agent stores is encrypted with it.
55494
- Losing it or changing it makes those secrets unreadable, with no recovery.
55806
+ Losing it or changing it makes those secrets unreadable, with no recovery. It
55807
+ also holds \`MINIO_ROOT_PASSWORD\` for the bundled object store, which the Compose
55808
+ file refuses to start without. The \`.env.example\` next to it carries the same
55809
+ variables with no values, so the folder can go into a repository while the \`.env\`
55810
+ stays out of it.
55495
55811
 
55496
55812
  **JSON payload**
55497
55813
 
@@ -55499,7 +55815,7 @@ Losing it or changing it makes those secrets unreadable, with no recovery.
55499
55815
  {
55500
55816
  "schema_version": 1,
55501
55817
  "outDir": "/abs/path/dist/deploy",
55502
- "files": ["Dockerfile", "docker-compose.yml", ".env", "README.md"],
55818
+ "files": ["Dockerfile", "docker-compose.yml", ".env", ".env.example", "README.md"],
55503
55819
  "imageTag": "acme-agent:latest",
55504
55820
  "baseImage": "ghcr.io/stackbone/stackbone-workspace-runtime:node24-base",
55505
55821
  "workflows": ["onboarding", "rag-ingest", "mapping-suggest"],
@@ -55533,6 +55849,7 @@ What lands in the output directory:
55533
55849
  \`\`\`text
55534
55850
  dist/workspace-bundle/
55535
55851
  .well-known/workspace.json # the manifest the runtime reads first
55852
+ .well-known/config/v1/config-schema.json # your config.schema.ts, as JSON Schema
55536
55853
  .well-known/workflow/v1/step.mjs # your compiled workflow steps
55537
55854
  .well-known/workflow/v1/workflow.vm.js # the workflow bundle the VM runs
55538
55855
  .well-known/workflow/v1/manifest.json # workflow ids, schemas, schedules
@@ -55552,6 +55869,14 @@ the container logs a warning naming the file and starts runs **unvalidated** —
55552
55869
  a start that your laptop rejects would be accepted in production. The warning is
55553
55870
  the thing to grep for after a deploy.
55554
55871
 
55872
+ \`config-schema.json\` is your \`config.schema.ts\`, converted to JSON Schema at
55873
+ build time. The container has no TypeScript, so it reads this file to render the
55874
+ config form in the dashboard and to validate every save. Write no
55875
+ \`config.schema.ts\` and the build ships no file, which is fine: config stays
55876
+ free-form. Write one the build cannot convert and it warns instead of failing,
55877
+ ships without the file, and the dashboard falls back to a raw JSON field with no
55878
+ validation on save. See [\`stackbone.config\`](/docs/sdk/config#typed-config).
55879
+
55555
55880
  Each deep agent is bundled with its own dependencies inlined, except for the
55556
55881
  packages that must stay a single copy per process. If one of your dependencies
55557
55882
  cannot survive bundling, keep it out with \`build.external\`. See
@@ -56837,19 +57162,20 @@ datastores locally as in production**. You import that client directly
56837
57162
  inside a tool's execution or a workflow \`'use step'\`; there is no handler
56838
57163
  wiring to configure.
56839
57164
 
56840
- | Variable | Points at |
56841
- | ------------------------- | ------------------------------------------------------------------------------------------ |
56842
- | \`PORT\` | The port the emulator listens on. |
56843
- | \`STACKBONE_POSTGRES_URL\` | Local Postgres on \`:5433\` with the \`stackbone_dev\` database. |
56844
- | \`STACKBONE_S3_ENDPOINT\` | MinIO on \`:9004\`. |
56845
- | \`STACKBONE_S3_BUCKET\` | \`stackbone-dev\`. |
56846
- | \`STACKBONE_S3_ACCESS_KEY\` | \`stackbone\`. |
56847
- | \`STACKBONE_S3_SECRET_KEY\` | \`stackbone-secret\`. |
56848
- | \`STACKBONE_S3_REGION\` | \`us-east-1\` (MinIO accepts any region). |
56849
- | \`STACKBONE_AGENT_ID\` | The slug of the workspace (the multi-tenant key prefix for storage and observability). |
56850
- | \`WORKFLOW_REDIS_URL\` | Local Redis on \`:6380\`, the durable backend the workflow runtime replays from. |
56851
- | \`MODEL_PROVIDER_API_KEY\` | The model-provider key, when one resolves (see below). Absent for a keyless local gateway. |
56852
- | \`MODEL_PROVIDER_BASE_URL\` | The OpenAI-compatible base URL, when one resolves. Absent means the OpenRouter default. |
57165
+ | Variable | Points at |
57166
+ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
57167
+ | \`PORT\` | The port the emulator listens on. |
57168
+ | \`STACKBONE_POSTGRES_URL\` | Local Postgres on \`:5433\` with the \`stackbone_dev\` database. |
57169
+ | \`STACKBONE_S3_ENDPOINT\` | MinIO on \`:9004\`. |
57170
+ | \`STACKBONE_S3_BUCKET\` | \`stackbone-dev\`. |
57171
+ | \`STACKBONE_S3_ACCESS_KEY\` | \`stackbone\`. |
57172
+ | \`STACKBONE_S3_SECRET_KEY\` | \`stackbone-secret\`. |
57173
+ | \`STACKBONE_S3_REGION\` | \`us-east-1\` (MinIO accepts any region). |
57174
+ | \`STACKBONE_S3_FORCE_PATH_STYLE\` | \`true\`, the addressing style MinIO speaks. See [\`stackbone.storage\`](/docs/sdk/storage#path-style-or-subdomain). |
57175
+ | \`STACKBONE_AGENT_ID\` | The slug of the workspace (the multi-tenant key prefix for storage and observability). |
57176
+ | \`WORKFLOW_REDIS_URL\` | Local Redis on \`:6380\`, the durable backend the workflow runtime replays from. |
57177
+ | \`MODEL_PROVIDER_API_KEY\` | The model-provider key, when one resolves (see below). Absent for a keyless local gateway. |
57178
+ | \`MODEL_PROVIDER_BASE_URL\` | The OpenAI-compatible base URL, when one resolves. Absent means the OpenRouter default. |
56853
57179
 
56854
57180
  > See [Configuration → Environment variables](/docs/cli/configuration#environment-variables)
56855
57181
  > for the full list.
@@ -58852,14 +59178,31 @@ A deployed agent never configures storage by hand. The runtime injects these
58852
59178
  environment variables when your container boots, and the ambient \`stackbone\`
58853
59179
  client reads them on first use:
58854
59180
 
58855
- | Env var the runtime injects | Holds |
58856
- | --------------------------- | -------------------------------------------------------------- |
58857
- | \`STACKBONE_S3_ACCESS_KEY\` | Access key for the agent's physical bucket. |
58858
- | \`STACKBONE_S3_SECRET_KEY\` | Secret access key. |
58859
- | \`STACKBONE_S3_ENDPOINT\` | S3-compatible endpoint (MinIO in \`stackbone dev\`). |
58860
- | \`STACKBONE_S3_BUCKET\` | The physical bucket your logical buckets are namespaced under. |
58861
- | \`STACKBONE_S3_REGION\` | Region, defaults to \`'us-east-1'\` when unset. |
58862
- | \`STACKBONE_AGENT_ID\` | The agent identity prepended to every key. |
59181
+ | Env var the runtime injects | Holds |
59182
+ | ------------------------------- | -------------------------------------------------------------- |
59183
+ | \`STACKBONE_S3_ACCESS_KEY\` | Access key for the agent's physical bucket. |
59184
+ | \`STACKBONE_S3_SECRET_KEY\` | Secret access key. |
59185
+ | \`STACKBONE_S3_ENDPOINT\` | S3-compatible endpoint (MinIO in \`stackbone dev\`). |
59186
+ | \`STACKBONE_S3_BUCKET\` | The physical bucket your logical buckets are namespaced under. |
59187
+ | \`STACKBONE_S3_REGION\` | Region, defaults to \`'us-east-1'\` when unset. |
59188
+ | \`STACKBONE_S3_FORCE_PATH_STYLE\` | How the bucket is addressed. Defaults to \`true\`. |
59189
+ | \`STACKBONE_AGENT_ID\` | The agent identity prepended to every key. |
59190
+
59191
+ ### Path-style or subdomain
59192
+
59193
+ \`STACKBONE_S3_FORCE_PATH_STYLE\` decides where the bucket name goes in the URL,
59194
+ and different backends disagree:
59195
+
59196
+ | Value | URL shape | Backends that want it |
59197
+ | ---------------- | ------------------------- | ----------------------- |
59198
+ | \`true\` (default) | \`https://host/bucket/key\` | MinIO, Cloudflare R2 |
59199
+ | \`false\` | \`https://bucket.host/key\` | AWS S3, Railway Buckets |
59200
+
59201
+ The default is \`true\` because that is what MinIO speaks, and MinIO is what
59202
+ \`stackbone dev\` runs. If your deployment points at a bucket that only answers to
59203
+ the subdomain form, every call fails with \`NoSuchBucket\` or a bare 403 even
59204
+ though the credentials are correct. That is the symptom of this one variable, so
59205
+ check it before you conclude the keys are wrong.
58863
59206
 
58864
59207
  If you need explicit configuration (for a local script or a test) you can
58865
59208
  pass an \`s3\` block to \`createClient()\` instead of relying on the injected env:
@@ -58873,7 +59216,8 @@ const sb = createClient({
58873
59216
  secretAccessKey: '…',
58874
59217
  endpoint: 'http://localhost:9000',
58875
59218
  bucket: 'my-bucket',
58876
- // region defaults to 'auto'
59219
+ // region defaults to 'us-east-1'
59220
+ // forcePathStyle defaults to true; set false for AWS S3
58877
59221
  },
58878
59222
  agentId: 'local-agent',
58879
59223
  });
@@ -58881,9 +59225,9 @@ const bucket = sb.storage.from('uploads');
58881
59225
  \`\`\`
58882
59226
 
58883
59227
  Missing any required value surfaces \`s3_credentials_missing\`,
58884
- \`s3_bucket_missing\` or \`agent_id_missing\` with a hint naming what to set:
58885
- \`region\` is the only one that never trips the missing-credential
58886
- errors because of the \`'auto'\` default.
59228
+ \`s3_bucket_missing\` or \`agent_id_missing\` with a hint naming what to set.
59229
+ \`region\` and \`forcePathStyle\` never trip those errors, because both carry a
59230
+ default (\`'us-east-1'\` and \`true\`).
58887
59231
 
58888
59232
  ## Upload, download, list, remove
58889
59233
 
@@ -60163,7 +60507,10 @@ export const configSchema = z.object({
60163
60507
 
60164
60508
  That one file drives three things at once: the form the operator sees in
60165
60509
  the dashboard, the validation applied when they save, and your types.
60166
- Generate the types with:
60510
+
60511
+ \`stackbone build\` converts the schema to JSON Schema and writes it into
60512
+ the bundle, so a deployed container renders the same form your laptop
60513
+ does. Generate the types with:
60167
60514
 
60168
60515
  \`\`\`sh
60169
60516
  stackbone config types
@@ -60650,22 +60997,23 @@ workspace works zero-config locally. Each variable falls back to a typed
60650
60997
  config field if you pass one to \`createClient\`, but you rarely need to:
60651
60998
  the runtime provides them all:
60652
60999
 
60653
- | Env var | Feeds | Provided by \`stackbone dev\` |
60654
- | --------------------------- | --------------------------------------- | --------------------------- |
60655
- | \`STACKBONE_API_URL\` | Control-plane calls | ✅ (local tunnel) |
60656
- | \`STACKBONE_INSTALLATION_ID\` | Identifies the install | ✅ |
60657
- | \`STACKBONE_AGENT_ID\` | Workspace identity + storage key prefix | ✅ |
60658
- | \`HMAC_SECRET\` | Signs Stackbone Connect broker calls | ✅ |
60659
- | \`STACKBONE_POSTGRES_URL\` | \`stackbone.database\` + \`stackbone.rag\` | ✅ (local Postgres) |
60660
- | \`STACKBONE_SECRET_KEY\` | Decrypts \`stackbone.secrets\` | ✅ |
60661
- | \`WORKFLOW_REDIS_URL\` | Durable workflow execution backend | ✅ (local Redis) |
60662
- | \`STACKBONE_S3_ENDPOINT\` | \`stackbone.storage\` | ✅ (local MinIO) |
60663
- | \`STACKBONE_S3_BUCKET\` | \`stackbone.storage\` | ✅ (\`stackbone-dev\`) |
60664
- | \`STACKBONE_S3_ACCESS_KEY\` | \`stackbone.storage\` | ✅ |
60665
- | \`STACKBONE_S3_SECRET_KEY\` | \`stackbone.storage\` | ✅ |
60666
- | \`STACKBONE_S3_REGION\` | \`stackbone.storage\` | ✅ |
60667
- | \`MODEL_PROVIDER_API_KEY\` | \`stackbone.ai\` | ❗ **you supply it** |
60668
- | \`MODEL_PROVIDER_BASE_URL\` | \`stackbone.ai\` (override) | ❗ **you supply it** |
61000
+ | Env var | Feeds | Provided by \`stackbone dev\` |
61001
+ | ------------------------------- | --------------------------------------- | --------------------------- |
61002
+ | \`STACKBONE_API_URL\` | Control-plane calls | ✅ (local tunnel) |
61003
+ | \`STACKBONE_INSTALLATION_ID\` | Identifies the install | ✅ |
61004
+ | \`STACKBONE_AGENT_ID\` | Workspace identity + storage key prefix | ✅ |
61005
+ | \`HMAC_SECRET\` | Signs Stackbone Connect broker calls | ✅ |
61006
+ | \`STACKBONE_POSTGRES_URL\` | \`stackbone.database\` + \`stackbone.rag\` | ✅ (local Postgres) |
61007
+ | \`STACKBONE_SECRET_KEY\` | Decrypts \`stackbone.secrets\` | ✅ |
61008
+ | \`WORKFLOW_REDIS_URL\` | Durable workflow execution backend | ✅ (local Redis) |
61009
+ | \`STACKBONE_S3_ENDPOINT\` | \`stackbone.storage\` | ✅ (local MinIO) |
61010
+ | \`STACKBONE_S3_BUCKET\` | \`stackbone.storage\` | ✅ (\`stackbone-dev\`) |
61011
+ | \`STACKBONE_S3_ACCESS_KEY\` | \`stackbone.storage\` | ✅ |
61012
+ | \`STACKBONE_S3_SECRET_KEY\` | \`stackbone.storage\` | ✅ |
61013
+ | \`STACKBONE_S3_REGION\` | \`stackbone.storage\` | ✅ |
61014
+ | \`STACKBONE_S3_FORCE_PATH_STYLE\` | \`stackbone.storage\` | ✅ (\`true\`) |
61015
+ | \`MODEL_PROVIDER_API_KEY\` | \`stackbone.ai\` | ❗ **you supply it** |
61016
+ | \`MODEL_PROVIDER_BASE_URL\` | \`stackbone.ai\` (override) | ❗ **you supply it** |
60669
61017
 
60670
61018
  The two model-provider values are the only ones you bring yourself, and you
60671
61019
  have two ways to do it: export them in your shell, or save them once on the
@@ -63333,7 +63681,7 @@ var createInitCommand = (ctx) => defineCommand({
63333
63681
  });
63334
63682
  //#endregion
63335
63683
  //#region src/services/deployments.service.ts
63336
- init_src$12();
63684
+ init_src$11();
63337
63685
  init_src$13();
63338
63686
  var registerDeployment = async (ctx, body) => {
63339
63687
  return parseResponse$1(deploymentResponseSchema, await ctx.http.post("/api/v1/deployments", body), "POST /api/v1/deployments");
@@ -63347,7 +63695,7 @@ var readIdentityVerification = async (response) => {
63347
63695
  return "unknown";
63348
63696
  }
63349
63697
  const state = body?.identityVerification;
63350
- return state === "armed" || state === "unarmed" ? state : "unknown";
63698
+ return state === "armed" || state === "unarmed" || state === "untaught" ? state : "unknown";
63351
63699
  };
63352
63700
  var verifyDeploymentSecret = async (args) => {
63353
63701
  const fetchImpl = args.fetchImpl ?? fetch;
@@ -63390,6 +63738,73 @@ var verifyDeploymentSecret = async (args) => {
63390
63738
  });
63391
63739
  return readIdentityVerification(response);
63392
63740
  };
63741
+ /**
63742
+ * STA-594/595 — tell the box which installation it answers for, the step between
63743
+ * proving the secret and writing the deployment.
63744
+ *
63745
+ * The browser wizard has always done this; `link` did not, and the box that
63746
+ * results is worse than unregistered: it refuses every browser and every CLI call
63747
+ * with a 401, because a box that was never taught answers for nothing. So this is
63748
+ * fail-closed like the challenge above it — a box that cannot be taught is not
63749
+ * registered at all, rather than registered and mute.
63750
+ *
63751
+ * Signed with the SAME secret the challenge proved, over METHOD.path.BODY, so the
63752
+ * exact bytes sent are the exact bytes signed: a body swapped in flight no longer
63753
+ * verifies and the box cannot be pointed at somebody else's installation.
63754
+ */
63755
+ var teachDeploymentIdentity = async (args) => {
63756
+ const fetchImpl = args.fetchImpl ?? fetch;
63757
+ const teachUrl = new URL(CONTROL_PLANE_IDENTITY_PATH, args.url).toString();
63758
+ const body = JSON.stringify({
63759
+ installationId: args.installationId,
63760
+ organizationId: args.organizationId
63761
+ });
63762
+ const header = buildStackboneSignatureHeader({
63763
+ secrets: [args.secret],
63764
+ payload: proxyRequestPayload({
63765
+ method: "POST",
63766
+ path: CONTROL_PLANE_IDENTITY_PATH,
63767
+ body
63768
+ })
63769
+ });
63770
+ let response;
63771
+ try {
63772
+ response = await fetchImpl(teachUrl, {
63773
+ method: "POST",
63774
+ headers: {
63775
+ "stackbone-signature": header,
63776
+ "content-type": "application/json"
63777
+ },
63778
+ body
63779
+ });
63780
+ } catch (cause) {
63781
+ throw new StackboneCliError({
63782
+ code: "generic",
63783
+ message: `Could not reach the deployed container at ${args.url} to tell it which installation it answers for`,
63784
+ suggestion: "Check the box is still reachable, then re-run `stackbone link`. Nothing was registered.",
63785
+ cause
63786
+ });
63787
+ }
63788
+ if (response.ok) return;
63789
+ if (response.status === 404) throw new StackboneCliError({
63790
+ code: "generic",
63791
+ message: `The container at ${args.url} runs an image from before boxes could be told which installation they answer for, so it would refuse every browser and every CLI call`,
63792
+ suggestion: "Upgrade the image and re-run `stackbone link`. Nothing was registered."
63793
+ });
63794
+ throw new StackboneCliError({
63795
+ code: "generic",
63796
+ message: `The container at ${args.url} could not record which installation it answers for: ${await readRefusal(response)}`,
63797
+ suggestion: "Fix what the box reported, then re-run `stackbone link`. Nothing was registered."
63798
+ });
63799
+ };
63800
+ /** The box's own sentence when it has one, and its status code when it does not. */
63801
+ var readRefusal = async (response) => {
63802
+ try {
63803
+ const message = (await response.json())?.message;
63804
+ if (typeof message === "string" && message.length > 0) return message;
63805
+ } catch {}
63806
+ return `it answered ${response.status}`;
63807
+ };
63393
63808
  //#endregion
63394
63809
  //#region src/services/handshake.service.ts
63395
63810
  init_src$13();
@@ -63497,6 +63912,7 @@ var requireLinkField = (value, flag, description) => {
63497
63912
  */
63498
63913
  var identityVerificationWarning = (state) => {
63499
63914
  if (state === "armed") return null;
63915
+ if (state === "untaught") return "This box verifies browser identity but has not been told which installation it answers for, so it refuses every browser and every CLI call. Register it — `stackbone link` does this for you.";
63500
63916
  if (state === "unarmed") return "This box does not verify browser identity, so Studio and the CLI cannot reach it — the deployment is registered, but every screen and every box command will be refused with a 401. Set STACKBONE_CONTROL_PLANE_URL on the container to your control plane address and restart it.";
63501
63917
  return "This box does not report whether it verifies browser identity — it runs an image from before boxes said so. The deployment is registered either way; upgrade the image to find out whether Studio can reach it at all.";
63502
63918
  };
@@ -63558,22 +63974,31 @@ var runLink = async (ctx, args) => {
63558
63974
  if (!ctx.flags.json) log.step("Reading the box protocol version");
63559
63975
  assertBoxCanBeRegistered(await readBoxContract({ url }));
63560
63976
  if (!ctx.flags.json) log.step("Verifying the --secret against the deployed container");
63561
- const identityVerification = await verifyDeploymentSecret({
63977
+ const challengeVerification = await verifyDeploymentSecret({
63562
63978
  url,
63563
63979
  secret
63564
63980
  });
63565
- const identityWarning = identityVerificationWarning(identityVerification);
63566
- if (identityWarning && !ctx.flags.json) log.warn(identityWarning);
63567
- const [localDev, deployment] = await Promise.all([upsertLocalDevInstallation(ctx, {
63981
+ const localDev = await upsertLocalDevInstallation(ctx, {
63568
63982
  creatorOrganizationSlug: activeOrg.slug,
63569
63983
  agentTemplateSlug: agent.slug
63570
- }), registerDeployment(ctx, {
63984
+ });
63985
+ if (!ctx.flags.json) log.step("Telling the box which installation it answers for");
63986
+ await teachDeploymentIdentity({
63987
+ url,
63988
+ secret,
63989
+ installationId: localDev.id,
63990
+ organizationId: localDev.organizationId
63991
+ });
63992
+ const identityVerification = challengeVerification === "untaught" ? "armed" : challengeVerification;
63993
+ const identityWarning = identityVerificationWarning(identityVerification);
63994
+ if (identityWarning && !ctx.flags.json) log.warn(identityWarning);
63995
+ const deployment = await registerDeployment(ctx, {
63571
63996
  organizationSlug: activeOrg.slug,
63572
63997
  agentSlug: agent.slug,
63573
63998
  url,
63574
63999
  hmacSecret: secret,
63575
64000
  imageTag: tag
63576
- })]);
64001
+ });
63577
64002
  await writeProjectConfig(ctx, agent, {
63578
64003
  root: targetDir,
63579
64004
  force: true