@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/CHANGELOG.md +72 -0
- package/main.js +842 -417
- package/migrations/0046_runtime_identity_control_plane.sql +28 -0
- package/package.json +1 -1
- package/stackbone-cli-0.3.3.tgz +0 -0
- package/stackbone-cli-0.3.2.tgz +0 -0
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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.
|
|
10513
|
-
*
|
|
10514
|
-
*
|
|
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
|
|
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
|
|
10556
|
+
return {
|
|
10557
|
+
ok: false,
|
|
10558
|
+
reason: "unverifiable"
|
|
10559
|
+
};
|
|
10532
10560
|
}
|
|
10533
10561
|
const identity = extractIdentity(payload);
|
|
10534
|
-
if (!identity) return
|
|
10535
|
-
|
|
10536
|
-
|
|
10537
|
-
|
|
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
|
-
|
|
10573
|
-
return
|
|
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
|
-
|
|
10583
|
-
|
|
10584
|
-
|
|
10585
|
-
|
|
10586
|
-
|
|
10587
|
-
|
|
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.
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
45021
|
-
init_src$
|
|
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" : "
|
|
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 = {
|
|
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.
|
|
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
|
-
*
|
|
46565
|
-
*
|
|
46566
|
-
*
|
|
46567
|
-
*
|
|
46568
|
-
*
|
|
46569
|
-
|
|
46570
|
-
|
|
46571
|
-
|
|
46572
|
-
|
|
46573
|
-
|
|
46574
|
-
|
|
46575
|
-
|
|
46576
|
-
|
|
46577
|
-
|
|
46578
|
-
|
|
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
|
-
|
|
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
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
49100
|
-
# Redis of its own. If you already run those, delete
|
|
49101
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
|
49165
|
-
# in — the
|
|
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
|
-
|
|
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
|
-
##
|
|
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 \`
|
|
49241
|
-
\`
|
|
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
|
|
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,
|
|
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
|
|
49425
|
-
"
|
|
49426
|
-
"
|
|
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
|
-
|
|
55419
|
-
brings up a Postgres and
|
|
55420
|
-
\`
|
|
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 #
|
|
55457
|
-
|
|
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
|
-
**
|
|
55475
|
-
README, and
|
|
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
|
|
56841
|
-
|
|
|
56842
|
-
| \`PORT\`
|
|
56843
|
-
| \`STACKBONE_POSTGRES_URL\`
|
|
56844
|
-
| \`STACKBONE_S3_ENDPOINT\`
|
|
56845
|
-
| \`STACKBONE_S3_BUCKET\`
|
|
56846
|
-
| \`STACKBONE_S3_ACCESS_KEY\`
|
|
56847
|
-
| \`STACKBONE_S3_SECRET_KEY\`
|
|
56848
|
-
| \`STACKBONE_S3_REGION\`
|
|
56849
|
-
| \`
|
|
56850
|
-
| \`
|
|
56851
|
-
| \`
|
|
56852
|
-
| \`
|
|
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
|
|
58856
|
-
|
|
|
58857
|
-
| \`STACKBONE_S3_ACCESS_KEY\`
|
|
58858
|
-
| \`STACKBONE_S3_SECRET_KEY\`
|
|
58859
|
-
| \`STACKBONE_S3_ENDPOINT\`
|
|
58860
|
-
| \`STACKBONE_S3_BUCKET\`
|
|
58861
|
-
| \`STACKBONE_S3_REGION\`
|
|
58862
|
-
| \`
|
|
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 '
|
|
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\`
|
|
58886
|
-
|
|
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
|
-
|
|
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
|
|
60654
|
-
|
|
|
60655
|
-
| \`STACKBONE_API_URL\`
|
|
60656
|
-
| \`STACKBONE_INSTALLATION_ID\`
|
|
60657
|
-
| \`STACKBONE_AGENT_ID\`
|
|
60658
|
-
| \`HMAC_SECRET\`
|
|
60659
|
-
| \`STACKBONE_POSTGRES_URL\`
|
|
60660
|
-
| \`STACKBONE_SECRET_KEY\`
|
|
60661
|
-
| \`WORKFLOW_REDIS_URL\`
|
|
60662
|
-
| \`STACKBONE_S3_ENDPOINT\`
|
|
60663
|
-
| \`STACKBONE_S3_BUCKET\`
|
|
60664
|
-
| \`STACKBONE_S3_ACCESS_KEY\`
|
|
60665
|
-
| \`STACKBONE_S3_SECRET_KEY\`
|
|
60666
|
-
| \`STACKBONE_S3_REGION\`
|
|
60667
|
-
| \`
|
|
60668
|
-
| \`
|
|
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$
|
|
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
|
|
63977
|
+
const challengeVerification = await verifyDeploymentSecret({
|
|
63562
63978
|
url,
|
|
63563
63979
|
secret
|
|
63564
63980
|
});
|
|
63565
|
-
const
|
|
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
|
-
})
|
|
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
|