@stackbone/cli 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/main.js
CHANGED
|
@@ -11,13 +11,13 @@ import pino from "pino";
|
|
|
11
11
|
import pretty from "pino-pretty";
|
|
12
12
|
import { exec, execFile, spawn, spawnSync } from "node:child_process";
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
14
|
+
import { DeleteObjectCommand, GetObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
14
15
|
import sodium from "libsodium-wrappers";
|
|
15
16
|
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
16
17
|
import { drizzle } from "drizzle-orm/postgres-js";
|
|
17
18
|
import postgres from "postgres";
|
|
18
19
|
import { extractText } from "unpdf";
|
|
19
20
|
import OpenAI from "openai";
|
|
20
|
-
import { DeleteObjectCommand, GetObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
21
21
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
22
22
|
import { format, promisify } from "node:util";
|
|
23
23
|
import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
@@ -153,7 +153,7 @@ var init_installations = __esmMin((() => {
|
|
|
153
153
|
organizationSlug: organizationSlugSchema,
|
|
154
154
|
agentSlug: z.string().min(1).max(80)
|
|
155
155
|
});
|
|
156
|
-
})), agentTemplateStatusSchema, agentTemplateResponseSchema, agentInstallationResponseSchema, DEFAULT_RAG_EMBEDDING_MODEL, agentManifestRagSchema, agentManifestProtocolSchema, runtimeSchema, agentManifestConnectionsSchema, recipeTriggerRefSchema, recipeActionRefSchema, agentManifestRecipeSchema, agentManifestAutomationsSchema, DEFAULT_AGENT_SCHEMA_PATH, DEFAULT_AGENT_MIGRATIONS_PATH, BUNDLE_MIGRATIONS_PATH, WORKSPACE_MANIFEST_PATH, WORKFLOW_WELL_KNOWN_DIR, WORKFLOW_STEP_FILE, WORKFLOW_VM_FILE, WORKFLOW_MANIFEST_FILE, WORKFLOW_ARTIFACTS, WORKFLOW_SCHEMAS_FILE, MANIFEST_ENTRY_KINDS, MANIFEST_ENTRY_MODES, MIN_IMAGE_VERSION_FOR_MANIFEST_V2, workflowStartSurface, workflowChatSurface, DEEP_AGENT_CHAT_SURFACE, agentManifestSchema;
|
|
156
|
+
})), agentTemplateStatusSchema, agentTemplateResponseSchema, agentInstallationResponseSchema, DEFAULT_RAG_EMBEDDING_MODEL, agentManifestRagSchema, agentManifestProtocolSchema, runtimeSchema, agentManifestConnectionsSchema, recipeTriggerRefSchema, recipeActionRefSchema, agentManifestRecipeSchema, agentManifestAutomationsSchema, DEFAULT_AGENT_SCHEMA_PATH, DEFAULT_AGENT_MIGRATIONS_PATH, BUNDLE_MIGRATIONS_PATH, WORKSPACE_MANIFEST_PATH, CONFIG_SCHEMA_ARTIFACT_PATH, WORKFLOW_WELL_KNOWN_DIR, WORKFLOW_STEP_FILE, WORKFLOW_VM_FILE, WORKFLOW_MANIFEST_FILE, WORKFLOW_ARTIFACTS, WORKFLOW_SCHEMAS_FILE, MANIFEST_ENTRY_KINDS, MANIFEST_ENTRY_MODES, MIN_IMAGE_VERSION_FOR_MANIFEST_V2, workflowStartSurface, workflowChatSurface, DEEP_AGENT_CHAT_SURFACE, agentManifestSchema;
|
|
157
157
|
var init_agents = __esmMin((() => {
|
|
158
158
|
init_json_object();
|
|
159
159
|
init_connectors();
|
|
@@ -270,6 +270,7 @@ var init_agents = __esmMin((() => {
|
|
|
270
270
|
DEFAULT_AGENT_MIGRATIONS_PATH = "./.stackbone/migrations";
|
|
271
271
|
BUNDLE_MIGRATIONS_PATH = ".well-known/migrations/v1";
|
|
272
272
|
WORKSPACE_MANIFEST_PATH = ".well-known/workspace.json";
|
|
273
|
+
CONFIG_SCHEMA_ARTIFACT_PATH = ".well-known/config/v1/config-schema.json";
|
|
273
274
|
WORKFLOW_WELL_KNOWN_DIR = ".well-known/workflow/v1";
|
|
274
275
|
WORKFLOW_STEP_FILE = "step.mjs";
|
|
275
276
|
WORKFLOW_VM_FILE = "workflow.vm.js";
|
|
@@ -501,6 +502,25 @@ var init_approvals = __esmMin((() => {
|
|
|
501
502
|
//#endregion
|
|
502
503
|
//#region ../../libs/shared/validators/src/lib/agents/contract.ts
|
|
503
504
|
/**
|
|
505
|
+
* Whether a box speaking protocol `version` can be registered as a deployment.
|
|
506
|
+
* Pure and total: the caller passes the number the handshake reported.
|
|
507
|
+
*/
|
|
508
|
+
function supportsDeploymentRegistration(version) {
|
|
509
|
+
return version >= 15;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* The one sentence both doors print when a box is below the floor.
|
|
513
|
+
*
|
|
514
|
+
* Shared, not copied, because the wizard and `stackbone link` refuse by the same
|
|
515
|
+
* rule and an operator who tries the other door must not read a different story.
|
|
516
|
+
* For the same reason it names no door: there is no "register it from the
|
|
517
|
+
* terminal instead" any more, and the only way out it can honestly offer is the
|
|
518
|
+
* one that works everywhere — run a current image.
|
|
519
|
+
*/
|
|
520
|
+
function deploymentRegistrationRefusal(version) {
|
|
521
|
+
return `This box speaks protocol version ${version}. Registering a deployment needs version 15 or later, because an older box refuses the signed call that proves its secret. Update the image the box runs to a current one and try again.`;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
504
524
|
* Validates that the negotiated `contract.version` is at or above the
|
|
505
525
|
* version the SDK still executes against. Pure, table-driven, never throws.
|
|
506
526
|
*
|
|
@@ -604,7 +624,7 @@ var init_deployments = __esmMin((() => {
|
|
|
604
624
|
agentSlug: agentSlugSchema,
|
|
605
625
|
url: z.url(),
|
|
606
626
|
hmacSecret: z.string().min(1),
|
|
607
|
-
imageTag: z.string().min(1).max(200)
|
|
627
|
+
imageTag: z.string().min(1).max(200).optional()
|
|
608
628
|
});
|
|
609
629
|
z.object({ url: z.url() });
|
|
610
630
|
deploymentResponseSchema = z.object({
|
|
@@ -3326,6 +3346,47 @@ var bootWorkspaceRuntime = async (opts, deps = {}) => {
|
|
|
3326
3346
|
};
|
|
3327
3347
|
};
|
|
3328
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
|
|
3329
3390
|
//#region ../../libs/sdk/src/config.ts
|
|
3330
3391
|
/**
|
|
3331
3392
|
* Single seam in the SDK for reading `process.env`. Every downstream module
|
|
@@ -3356,6 +3417,7 @@ function resolveConfig(config) {
|
|
|
3356
3417
|
s3Endpoint: config.s3?.endpoint ?? env["STACKBONE_S3_ENDPOINT"],
|
|
3357
3418
|
s3Bucket: config.s3?.bucket ?? env["STACKBONE_S3_BUCKET"],
|
|
3358
3419
|
s3Region: config.s3?.region ?? env["STACKBONE_S3_REGION"] ?? "us-east-1",
|
|
3420
|
+
s3ForcePathStyle: config.s3?.forcePathStyle ?? parseForcePathStyle(env["STACKBONE_S3_FORCE_PATH_STYLE"]),
|
|
3359
3421
|
otelExporterOtlpEndpoint: config.otel?.exporterOtlpEndpoint ?? env["OTEL_EXPORTER_OTLP_ENDPOINT"],
|
|
3360
3422
|
requireContract: env["STACKBONE_REQUIRE_CONTRACT"] !== "0",
|
|
3361
3423
|
contractTtlMs: parseTtlMs(env["STACKBONE_CONTRACT_TTL_MS"]),
|
|
@@ -3375,7 +3437,9 @@ function parseTtlMs(raw) {
|
|
|
3375
3437
|
if (!Number.isFinite(n) || n <= 0) return void 0;
|
|
3376
3438
|
return n;
|
|
3377
3439
|
}
|
|
3378
|
-
var init_config$1 = __esmMin((() => {
|
|
3440
|
+
var init_config$1 = __esmMin((() => {
|
|
3441
|
+
init_src$12();
|
|
3442
|
+
}));
|
|
3379
3443
|
//#endregion
|
|
3380
3444
|
//#region ../../libs/sdk/src/result.ts
|
|
3381
3445
|
function ok$1(data) {
|
|
@@ -3461,7 +3525,7 @@ function createContractStore(resolved = resolveConfig({})) {
|
|
|
3461
3525
|
};
|
|
3462
3526
|
}
|
|
3463
3527
|
async function performHandshake(baseUrl, fetchImpl) {
|
|
3464
|
-
const url = `${baseUrl}${CONTRACT_PATH}`;
|
|
3528
|
+
const url = `${baseUrl}${CONTRACT_PATH$1}`;
|
|
3465
3529
|
let response;
|
|
3466
3530
|
try {
|
|
3467
3531
|
response = await fetchImpl(url, { method: "GET" });
|
|
@@ -3547,12 +3611,12 @@ function maybeLogDebug(debugLogged, key, contract) {
|
|
|
3547
3611
|
debugLogged.add(key);
|
|
3548
3612
|
console.warn(`[stackbone] contract handshake ok: baseUrl=${key} version=${contract.version} capabilities=${contract.capabilities.length}`);
|
|
3549
3613
|
}
|
|
3550
|
-
var CONTRACT_PATH;
|
|
3614
|
+
var CONTRACT_PATH$1;
|
|
3551
3615
|
var init_contract_handshake = __esmMin((() => {
|
|
3552
3616
|
init_src$13();
|
|
3553
3617
|
init_config$1();
|
|
3554
3618
|
init_result$1();
|
|
3555
|
-
CONTRACT_PATH = "/api/contract";
|
|
3619
|
+
CONTRACT_PATH$1 = "/api/contract";
|
|
3556
3620
|
}));
|
|
3557
3621
|
//#endregion
|
|
3558
3622
|
//#region ../../libs/sdk/src/contract/capability-registry.ts
|
|
@@ -3958,7 +4022,7 @@ var init_stackbone_signature = __esmMin((() => {
|
|
|
3958
4022
|
}));
|
|
3959
4023
|
//#endregion
|
|
3960
4024
|
//#region ../../libs/shared/crypto/src/index.ts
|
|
3961
|
-
var init_src$
|
|
4025
|
+
var init_src$11 = __esmMin((() => {
|
|
3962
4026
|
init_secret_cipher();
|
|
3963
4027
|
init_constant_time();
|
|
3964
4028
|
init_workflow_signature();
|
|
@@ -4035,7 +4099,7 @@ function isDecision(value) {
|
|
|
4035
4099
|
}
|
|
4036
4100
|
var SIGNATURE_HEADER, DEFAULT_TOLERANCE_SECONDS, FUTURE_DRIFT_TOLERANCE_SECONDS;
|
|
4037
4101
|
var init_verify = __esmMin((() => {
|
|
4038
|
-
init_src$
|
|
4102
|
+
init_src$11();
|
|
4039
4103
|
init_result$1();
|
|
4040
4104
|
init_approval();
|
|
4041
4105
|
SIGNATURE_HEADER = STACKBONE_SIGNATURE_HEADER;
|
|
@@ -4723,7 +4787,7 @@ var init_compile = __esmMin((() => {
|
|
|
4723
4787
|
}));
|
|
4724
4788
|
//#endregion
|
|
4725
4789
|
//#region ../../libs/shared/prompt-compiler/src/index.ts
|
|
4726
|
-
var init_src$
|
|
4790
|
+
var init_src$10 = __esmMin((() => {
|
|
4727
4791
|
init_compile();
|
|
4728
4792
|
}));
|
|
4729
4793
|
//#endregion
|
|
@@ -4793,7 +4857,7 @@ function toIso$2(value) {
|
|
|
4793
4857
|
}
|
|
4794
4858
|
var PG_UNDEFINED_TABLE$4, PG_UNIQUE_VIOLATION$2, PromptsFacade;
|
|
4795
4859
|
var init_prompts = __esmMin((() => {
|
|
4796
|
-
init_src$
|
|
4860
|
+
init_src$10();
|
|
4797
4861
|
init_result$1();
|
|
4798
4862
|
PG_UNDEFINED_TABLE$4 = "42P01";
|
|
4799
4863
|
PG_UNIQUE_VIOLATION$2 = "23505";
|
|
@@ -5914,7 +5978,7 @@ var init_jobs = __esmMin((() => {
|
|
|
5914
5978
|
}));
|
|
5915
5979
|
//#endregion
|
|
5916
5980
|
//#region ../../libs/shared/rag-core/src/index.ts
|
|
5917
|
-
var init_src$
|
|
5981
|
+
var init_src$9 = __esmMin((() => {
|
|
5918
5982
|
init_result();
|
|
5919
5983
|
init_limits();
|
|
5920
5984
|
init_rag_contract();
|
|
@@ -6129,7 +6193,7 @@ function resolveRetrieveModel(request, resolved) {
|
|
|
6129
6193
|
}
|
|
6130
6194
|
var DEFAULT_BATCH_SIZE, RagModule;
|
|
6131
6195
|
var init_rag = __esmMin((() => {
|
|
6132
|
-
init_src$
|
|
6196
|
+
init_src$9();
|
|
6133
6197
|
init_src$13();
|
|
6134
6198
|
init_module_gate();
|
|
6135
6199
|
init_result$1();
|
|
@@ -6299,7 +6363,7 @@ var init_rag = __esmMin((() => {
|
|
|
6299
6363
|
//#region ../../libs/sdk/src/surfaces/agent-local/secrets/secrets.ts
|
|
6300
6364
|
var SecretsFacade;
|
|
6301
6365
|
var init_secrets = __esmMin((() => {
|
|
6302
|
-
init_src$
|
|
6366
|
+
init_src$11();
|
|
6303
6367
|
init_result$1();
|
|
6304
6368
|
SecretsFacade = class {
|
|
6305
6369
|
constructor(_resolved, _getDatabase) {
|
|
@@ -7104,35 +7168,6 @@ var init_call_connector = __esmMin((() => {
|
|
|
7104
7168
|
connection = ((id) => connectorHandle(id));
|
|
7105
7169
|
}));
|
|
7106
7170
|
//#endregion
|
|
7107
|
-
//#region ../../libs/shared/s3-client/src/lib/create-s3-compat-client.ts
|
|
7108
|
-
/**
|
|
7109
|
-
* The single S3-compatible client factory. Behind this seam it always sets
|
|
7110
|
-
* `forcePathStyle: true` (MinIO and most S3-compatible backends do not support
|
|
7111
|
-
* virtual-host buckets), falls back to `'us-east-1'` when the caller pins no
|
|
7112
|
-
* region, and shapes the flat `accessKeyId` / `secretAccessKey` pair into the
|
|
7113
|
-
* SDK's `credentials` object.
|
|
7114
|
-
*
|
|
7115
|
-
* Call sites pin their region explicitly: studio and the SDK pass the one they
|
|
7116
|
-
* resolved, the emulator pins `'us-east-1'`.
|
|
7117
|
-
*/
|
|
7118
|
-
function createS3CompatClient(options) {
|
|
7119
|
-
return new S3Client({
|
|
7120
|
-
endpoint: options.endpoint,
|
|
7121
|
-
region: options.region ?? "us-east-1",
|
|
7122
|
-
forcePathStyle: true,
|
|
7123
|
-
credentials: {
|
|
7124
|
-
accessKeyId: options.accessKeyId,
|
|
7125
|
-
secretAccessKey: options.secretAccessKey
|
|
7126
|
-
}
|
|
7127
|
-
});
|
|
7128
|
-
}
|
|
7129
|
-
var init_create_s3_compat_client = __esmMin((() => {}));
|
|
7130
|
-
//#endregion
|
|
7131
|
-
//#region ../../libs/shared/s3-client/src/index.ts
|
|
7132
|
-
var init_src$9 = __esmMin((() => {
|
|
7133
|
-
init_create_s3_compat_client();
|
|
7134
|
-
}));
|
|
7135
|
-
//#endregion
|
|
7136
7171
|
//#region ../../libs/sdk/src/surfaces/external/storage/storage.ts
|
|
7137
7172
|
function stripEtag$1(etag) {
|
|
7138
7173
|
return etag?.replace(/^"|"$/g, "");
|
|
@@ -7161,7 +7196,7 @@ function toStorageError(message, cause) {
|
|
|
7161
7196
|
}
|
|
7162
7197
|
var DEFAULT_SIGNED_URL_TTL_SECONDS, StorageModule, StorageBucket, S3_CREDENTIALS_MISSING_MESSAGE, S3_BUCKET_MISSING_MESSAGE, AGENT_ID_MISSING_MESSAGE;
|
|
7163
7198
|
var init_storage = __esmMin((() => {
|
|
7164
|
-
init_src$
|
|
7199
|
+
init_src$12();
|
|
7165
7200
|
init_module_gate();
|
|
7166
7201
|
init_result$1();
|
|
7167
7202
|
DEFAULT_SIGNED_URL_TTL_SECONDS = 3600;
|
|
@@ -7197,7 +7232,8 @@ var init_storage = __esmMin((() => {
|
|
|
7197
7232
|
endpoint,
|
|
7198
7233
|
region,
|
|
7199
7234
|
accessKeyId,
|
|
7200
|
-
secretAccessKey
|
|
7235
|
+
secretAccessKey,
|
|
7236
|
+
forcePathStyle: this._resolved.s3ForcePathStyle
|
|
7201
7237
|
});
|
|
7202
7238
|
return ok$1({
|
|
7203
7239
|
client: this._s3,
|
|
@@ -7713,7 +7749,7 @@ function createDatabaseProfilePersistence() {
|
|
|
7713
7749
|
}
|
|
7714
7750
|
var AGENT_IDENTITY, errMessage$23, BrowserAuthProfileStore;
|
|
7715
7751
|
var init_browser_profile_store = __esmMin((() => {
|
|
7716
|
-
init_src$
|
|
7752
|
+
init_src$11();
|
|
7717
7753
|
init_result$1();
|
|
7718
7754
|
init_ambient_client();
|
|
7719
7755
|
init_browser_profile_state();
|
|
@@ -8388,7 +8424,7 @@ var init_src$8 = __esmMin((() => {
|
|
|
8388
8424
|
init_browser_tools();
|
|
8389
8425
|
init_browser_mode_resolver();
|
|
8390
8426
|
init_browser_profile_store();
|
|
8391
|
-
init_src$
|
|
8427
|
+
init_src$11();
|
|
8392
8428
|
init_ambient_client();
|
|
8393
8429
|
}));
|
|
8394
8430
|
//#endregion
|
|
@@ -10197,6 +10233,7 @@ var resolveCorsAllowlist = (input) => {
|
|
|
10197
10233
|
};
|
|
10198
10234
|
//#endregion
|
|
10199
10235
|
//#region ../../libs/runtime/workspace-runtime/src/lib/server/agent-config.ts
|
|
10236
|
+
init_src$13();
|
|
10200
10237
|
var StudioAgentConfigValidationError = class extends Error {
|
|
10201
10238
|
constructor(errors) {
|
|
10202
10239
|
super("agent_config_invalid");
|
|
@@ -10211,15 +10248,6 @@ var StudioAgentConfigVersionNotFoundError = class extends Error {
|
|
|
10211
10248
|
}
|
|
10212
10249
|
};
|
|
10213
10250
|
/**
|
|
10214
|
-
* Literal location of the config JSON Schema the dev build extracts from the
|
|
10215
|
-
* creator's `config.schema.ts`, relative to the project root. Shared artifact
|
|
10216
|
-
* contract across the whole feature (`config_schema_dynamic_config`): the CLI
|
|
10217
|
-
* extractor writes it, the SDK type codegen reads it, and the emulator (here)
|
|
10218
|
-
* resolves it for the Config editor + write validation. Kept as a literal so the
|
|
10219
|
-
* emulator never imports `apps/cli`.
|
|
10220
|
-
*/
|
|
10221
|
-
var CONFIG_SCHEMA_ARTIFACT_PATH = ".well-known/config/v1/config-schema.json";
|
|
10222
|
-
/**
|
|
10223
10251
|
* Read the extracted config JSON Schema off disk for a given project root.
|
|
10224
10252
|
*
|
|
10225
10253
|
* Reads `<projectRoot>/.well-known/config/v1/config-schema.json` (the build
|
|
@@ -10415,15 +10443,17 @@ var init_well_known = __esmMin((() => {
|
|
|
10415
10443
|
}));
|
|
10416
10444
|
//#endregion
|
|
10417
10445
|
//#region ../../libs/runtime/workspace-runtime/src/lib/server/box-auth-gate.ts
|
|
10418
|
-
init_src$
|
|
10446
|
+
init_src$11();
|
|
10419
10447
|
init_well_known();
|
|
10420
10448
|
var PROXY_CHALLENGE_PATH = "/api/proxy-challenge";
|
|
10449
|
+
var CONTROL_PLANE_IDENTITY_PATH = "/api/control-plane-identity";
|
|
10421
10450
|
var PUBLIC_PREFIXES = [
|
|
10422
10451
|
"/api/contract",
|
|
10423
10452
|
"/api/health",
|
|
10424
10453
|
"/live",
|
|
10425
10454
|
"/health",
|
|
10426
10455
|
PROXY_CHALLENGE_PATH,
|
|
10456
|
+
CONTROL_PLANE_IDENTITY_PATH,
|
|
10427
10457
|
"/api/connect/callback",
|
|
10428
10458
|
"/api/connect/connected",
|
|
10429
10459
|
"/api/connect/token",
|
|
@@ -10470,6 +10500,14 @@ var rejectToken = (c) => c.json({
|
|
|
10470
10500
|
code: "invalid_token",
|
|
10471
10501
|
message: "Identity token is missing, expired, or invalid."
|
|
10472
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);
|
|
10473
10511
|
var BEARER_RE = /^Bearer\s+(.+)$/i;
|
|
10474
10512
|
var extractBearer = (header) => {
|
|
10475
10513
|
if (!header) return null;
|
|
@@ -10490,9 +10528,12 @@ var extractIdentity = (payload) => {
|
|
|
10490
10528
|
};
|
|
10491
10529
|
/**
|
|
10492
10530
|
* Build the identity-token verifier ONCE, so the JWKS resolver (and its in-memory
|
|
10493
|
-
* cache) lives for the process instead of being rebuilt per request.
|
|
10494
|
-
*
|
|
10495
|
-
*
|
|
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.
|
|
10496
10537
|
*/
|
|
10497
10538
|
var createIdentityTokenVerifier = (options) => {
|
|
10498
10539
|
const getKey = options.createJwks?.() ?? createRemoteJWKSet(new URL(options.jwksUrl), {
|
|
@@ -10501,7 +10542,10 @@ var createIdentityTokenVerifier = (options) => {
|
|
|
10501
10542
|
});
|
|
10502
10543
|
return async (c) => {
|
|
10503
10544
|
const token = extractBearer(c.req.header("authorization"));
|
|
10504
|
-
if (!token) return
|
|
10545
|
+
if (!token) return {
|
|
10546
|
+
ok: false,
|
|
10547
|
+
reason: "absent"
|
|
10548
|
+
};
|
|
10505
10549
|
let payload;
|
|
10506
10550
|
try {
|
|
10507
10551
|
({payload} = await jwtVerify(token, getKey, {
|
|
@@ -10509,13 +10553,41 @@ var createIdentityTokenVerifier = (options) => {
|
|
|
10509
10553
|
audience: options.audience
|
|
10510
10554
|
}));
|
|
10511
10555
|
} catch {
|
|
10512
|
-
return
|
|
10556
|
+
return {
|
|
10557
|
+
ok: false,
|
|
10558
|
+
reason: "unverifiable"
|
|
10559
|
+
};
|
|
10513
10560
|
}
|
|
10514
10561
|
const identity = extractIdentity(payload);
|
|
10515
|
-
if (!identity) return
|
|
10516
|
-
|
|
10517
|
-
|
|
10518
|
-
|
|
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
|
+
};
|
|
10519
10591
|
};
|
|
10520
10592
|
};
|
|
10521
10593
|
/**
|
|
@@ -10549,9 +10621,12 @@ var createBoxAuthGate = (options) => {
|
|
|
10549
10621
|
const proxySecret = options.proxyAuth?.secret;
|
|
10550
10622
|
const verifyIdentityToken = options.jwtAuth ? createIdentityTokenVerifier(options.jwtAuth) : void 0;
|
|
10551
10623
|
if (!proxySecret && !verifyIdentityToken) return async (_c, next) => next();
|
|
10552
|
-
const reject = (c) => {
|
|
10553
|
-
|
|
10554
|
-
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);
|
|
10555
10630
|
};
|
|
10556
10631
|
return async (c, next) => {
|
|
10557
10632
|
if (c.req.method === "OPTIONS") return next();
|
|
@@ -10560,23 +10635,21 @@ var createBoxAuthGate = (options) => {
|
|
|
10560
10635
|
c.set(AUTHENTICATED_CALLER_VAR, { provenance: "proxy" });
|
|
10561
10636
|
return next();
|
|
10562
10637
|
}
|
|
10563
|
-
|
|
10564
|
-
|
|
10565
|
-
|
|
10566
|
-
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
|
|
10570
|
-
return next();
|
|
10571
|
-
}
|
|
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();
|
|
10572
10645
|
}
|
|
10573
|
-
return reject(c);
|
|
10646
|
+
return reject(c, check);
|
|
10574
10647
|
};
|
|
10575
10648
|
};
|
|
10576
10649
|
//#endregion
|
|
10577
10650
|
//#region ../../libs/runtime/workspace-runtime/src/lib/server/contract.ts
|
|
10578
10651
|
init_src$13();
|
|
10579
|
-
var STACKBONE_CLI_BUILD_VERSION = "0.3.
|
|
10652
|
+
var STACKBONE_CLI_BUILD_VERSION = "0.3.3";
|
|
10580
10653
|
/**
|
|
10581
10654
|
* Capabilities the local emulator advertises on `GET /api/contract`, derived
|
|
10582
10655
|
* from the route registry above so the advertised set and the mounted routes
|
|
@@ -10597,7 +10670,7 @@ var EMULATOR_CAPABILITIES = Object.keys({
|
|
|
10597
10670
|
"browser.provider": "/api/browser-provider"
|
|
10598
10671
|
});
|
|
10599
10672
|
var buildContractResponse = () => contractResponseSchema.parse({
|
|
10600
|
-
version:
|
|
10673
|
+
version: 15,
|
|
10601
10674
|
minSupported: 1,
|
|
10602
10675
|
capabilities: [...EMULATOR_CAPABILITIES],
|
|
10603
10676
|
build: {
|
|
@@ -10975,7 +11048,7 @@ async function runLogTail(stream, filters, startTailer) {
|
|
|
10975
11048
|
}
|
|
10976
11049
|
//#endregion
|
|
10977
11050
|
//#region ../../libs/runtime/workspace-runtime/src/lib/handlers/actor.ts
|
|
10978
|
-
init_src$
|
|
11051
|
+
init_src$11();
|
|
10979
11052
|
function readActorUserId(c) {
|
|
10980
11053
|
const verified = readVerifiedIdentity(c);
|
|
10981
11054
|
if (verified?.userId) return verified.userId;
|
|
@@ -11036,7 +11109,7 @@ var requireCapability = (capability) => {
|
|
|
11036
11109
|
};
|
|
11037
11110
|
//#endregion
|
|
11038
11111
|
//#region ../../libs/runtime/workspace-runtime/src/lib/handlers/prompts-handlers.ts
|
|
11039
|
-
init_src$
|
|
11112
|
+
init_src$10();
|
|
11040
11113
|
init_src$13();
|
|
11041
11114
|
var PG_UNIQUE_VIOLATION$1 = "23505";
|
|
11042
11115
|
var withClient$3 = async (opts, fn) => {
|
|
@@ -11327,8 +11400,8 @@ var createPromptsRouter = (opts) => {
|
|
|
11327
11400
|
};
|
|
11328
11401
|
//#endregion
|
|
11329
11402
|
//#region ../../libs/runtime/workspace-runtime/src/lib/services/storage.ts
|
|
11330
|
-
init_src$10();
|
|
11331
11403
|
init_src$9();
|
|
11404
|
+
init_src$12();
|
|
11332
11405
|
var ObjectNotFoundError = class extends Error {
|
|
11333
11406
|
constructor(message = "object_not_found") {
|
|
11334
11407
|
super(message);
|
|
@@ -11339,7 +11412,8 @@ var buildClient = (options) => createS3CompatClient({
|
|
|
11339
11412
|
endpoint: options.endpoint,
|
|
11340
11413
|
region: options.region || "us-east-1",
|
|
11341
11414
|
accessKeyId: options.accessKey,
|
|
11342
|
-
secretAccessKey: options.secretKey
|
|
11415
|
+
secretAccessKey: options.secretKey,
|
|
11416
|
+
...options.forcePathStyle === void 0 ? {} : { forcePathStyle: options.forcePathStyle }
|
|
11343
11417
|
});
|
|
11344
11418
|
var listBuckets$1 = async (options) => ({ items: [{
|
|
11345
11419
|
name: options.bucket,
|
|
@@ -11595,7 +11669,7 @@ var createStorageRouter = (storageOptions) => {
|
|
|
11595
11669
|
};
|
|
11596
11670
|
//#endregion
|
|
11597
11671
|
//#region ../../libs/runtime/workspace-runtime/src/lib/services/secrets.ts
|
|
11598
|
-
init_src$
|
|
11672
|
+
init_src$11();
|
|
11599
11673
|
init_src$13();
|
|
11600
11674
|
var StudioSecretNotFoundError = class extends Error {
|
|
11601
11675
|
constructor() {
|
|
@@ -24816,7 +24890,7 @@ var init_resume_interrupt = __esmMin((() => {
|
|
|
24816
24890
|
}));
|
|
24817
24891
|
//#endregion
|
|
24818
24892
|
//#region ../../libs/runtime/workspace-runtime/src/lib/evals/eval-approval-answer.ts
|
|
24819
|
-
init_src$
|
|
24893
|
+
init_src$11();
|
|
24820
24894
|
init_resume_interrupt();
|
|
24821
24895
|
init_guardrail_park();
|
|
24822
24896
|
/** The route the box records a decision on. */
|
|
@@ -27185,7 +27259,7 @@ var createSessionsRouter = (deps) => {
|
|
|
27185
27259
|
};
|
|
27186
27260
|
//#endregion
|
|
27187
27261
|
//#region ../../libs/runtime/workspace-runtime/src/lib/handlers/approval-callback.ts
|
|
27188
|
-
init_src$
|
|
27262
|
+
init_src$11();
|
|
27189
27263
|
init_src$13();
|
|
27190
27264
|
var buildCallbackPayload = (approval, decision) => ({
|
|
27191
27265
|
status: decision.decision === "approve" ? "approved" : "rejected",
|
|
@@ -28293,7 +28367,7 @@ var mapRunToRagJob = (run) => ({
|
|
|
28293
28367
|
});
|
|
28294
28368
|
//#endregion
|
|
28295
28369
|
//#region ../../libs/runtime/workspace-runtime/src/lib/handlers/rag-handlers.ts
|
|
28296
|
-
init_src$
|
|
28370
|
+
init_src$9();
|
|
28297
28371
|
init_src$13();
|
|
28298
28372
|
var RAG_DIMENSIONS$1 = 1536;
|
|
28299
28373
|
var RAG_DISTANCE$1 = "cosine";
|
|
@@ -28571,7 +28645,7 @@ var init_readiness$1 = __esmMin((() => {
|
|
|
28571
28645
|
}));
|
|
28572
28646
|
//#endregion
|
|
28573
28647
|
//#region ../../libs/runtime/workspace-runtime/src/lib/handlers/rag-write-handlers.ts
|
|
28574
|
-
init_src$
|
|
28648
|
+
init_src$9();
|
|
28575
28649
|
init_readiness$1();
|
|
28576
28650
|
var createMinioRagWriteStorage = (storage) => ({
|
|
28577
28651
|
putManagedObject: async (key, body, contentType) => {
|
|
@@ -28837,7 +28911,7 @@ var basename$1 = (key) => {
|
|
|
28837
28911
|
//#endregion
|
|
28838
28912
|
//#region ../../libs/runtime/workspace-runtime/src/lib/handlers/rag-router.ts
|
|
28839
28913
|
init_src$13();
|
|
28840
|
-
init_src$
|
|
28914
|
+
init_src$9();
|
|
28841
28915
|
/**
|
|
28842
28916
|
* Builds the Hono router mounted at `/api/rag` by the emulator server. Studio RAG
|
|
28843
28917
|
* explorer + managed WRITE surface (V9 + feature 30, F76/F5). Reads/writes the
|
|
@@ -32393,7 +32467,7 @@ var init_src$1 = __esmMin((() => {
|
|
|
32393
32467
|
init_token_cache_key();
|
|
32394
32468
|
init_get_token();
|
|
32395
32469
|
init_src$3();
|
|
32396
|
-
init_src$
|
|
32470
|
+
init_src$11();
|
|
32397
32471
|
init_schema();
|
|
32398
32472
|
init_grant_refresh_ports();
|
|
32399
32473
|
init_grant_refresh_runtime();
|
|
@@ -32459,7 +32533,7 @@ function errorMessage$1(err) {
|
|
|
32459
32533
|
}
|
|
32460
32534
|
//#endregion
|
|
32461
32535
|
//#region ../../libs/runtime/workspace-runtime/src/lib/handlers/connect-handlers.ts
|
|
32462
|
-
init_src$
|
|
32536
|
+
init_src$11();
|
|
32463
32537
|
init_src$1();
|
|
32464
32538
|
init_src$3();
|
|
32465
32539
|
init_src$2();
|
|
@@ -40152,7 +40226,7 @@ var init_src = __esmMin((() => {
|
|
|
40152
40226
|
//#region ../../libs/runtime/workspace-runtime/src/lib/catalog/system-entries.ts
|
|
40153
40227
|
var RESERVED_ROLES, isSystemEntry;
|
|
40154
40228
|
var init_system_entries = __esmMin((() => {
|
|
40155
|
-
init_src$
|
|
40229
|
+
init_src$9();
|
|
40156
40230
|
init_src();
|
|
40157
40231
|
RESERVED_ROLES = {
|
|
40158
40232
|
agent: new Set([RAG_INGEST_ROLE]),
|
|
@@ -40738,8 +40812,7 @@ var redisConnection = (redisUrl) => {
|
|
|
40738
40812
|
const db = path === "" ? void 0 : Number(path);
|
|
40739
40813
|
if (db !== void 0 && !Number.isInteger(db)) throw new Error(`Invalid Redis database in URL path: '${url.pathname}' (expected an integer)`);
|
|
40740
40814
|
return {
|
|
40741
|
-
|
|
40742
|
-
port: url.port ? Number(url.port) : 6379,
|
|
40815
|
+
url: redisUrl,
|
|
40743
40816
|
...db === void 0 ? {} : { db },
|
|
40744
40817
|
maxRetriesPerRequest: null
|
|
40745
40818
|
};
|
|
@@ -41152,6 +41225,7 @@ var init_connections = __esmMin((() => {
|
|
|
41152
41225
|
s3SecretKey: "STACKBONE_S3_SECRET_KEY",
|
|
41153
41226
|
s3Bucket: "STACKBONE_S3_BUCKET",
|
|
41154
41227
|
s3Region: "STACKBONE_S3_REGION",
|
|
41228
|
+
s3ForcePathStyle: "STACKBONE_S3_FORCE_PATH_STYLE",
|
|
41155
41229
|
agentConfig: "STACKBONE_AGENT_CONFIG",
|
|
41156
41230
|
port: "PORT",
|
|
41157
41231
|
mode: "STACKBONE_MODE",
|
|
@@ -41160,7 +41234,7 @@ var init_connections = __esmMin((() => {
|
|
|
41160
41234
|
buildAgentEnv = (base, injection, options) => {
|
|
41161
41235
|
const env = {
|
|
41162
41236
|
...base,
|
|
41163
|
-
[STACKBONE_ENV_KEYS.contractVersion]: String(
|
|
41237
|
+
[STACKBONE_ENV_KEYS.contractVersion]: String(15),
|
|
41164
41238
|
[STACKBONE_ENV_KEYS.agentId]: injection.agentId,
|
|
41165
41239
|
[STACKBONE_ENV_KEYS.postgresUrl]: injection.services.postgresUrl,
|
|
41166
41240
|
[STACKBONE_ENV_KEYS.s3Endpoint]: injection.services.s3Endpoint,
|
|
@@ -41168,6 +41242,7 @@ var init_connections = __esmMin((() => {
|
|
|
41168
41242
|
[STACKBONE_ENV_KEYS.s3SecretKey]: injection.services.s3SecretKey,
|
|
41169
41243
|
[STACKBONE_ENV_KEYS.s3Bucket]: injection.services.s3Bucket,
|
|
41170
41244
|
[STACKBONE_ENV_KEYS.s3Region]: injection.services.s3Region,
|
|
41245
|
+
[STACKBONE_ENV_KEYS.s3ForcePathStyle]: String(injection.services.s3ForcePathStyle),
|
|
41171
41246
|
[STACKBONE_ENV_KEYS.port]: String(injection.agentPort),
|
|
41172
41247
|
[STACKBONE_ENV_KEYS.mode]: options.mode,
|
|
41173
41248
|
[STACKBONE_ENV_KEYS.secretKey]: options.secretKey
|
|
@@ -42777,7 +42852,7 @@ async function failApprovalHookStep(params) {
|
|
|
42777
42852
|
var deepAgentBridgeLogger, deriveWorkflowSteps, degradedReasonFor, resolveWorkflowRefs, applySdkContractEnv, extractWorldRunIdFromFlowBody, loadProjectRuntime, mountWorkflowRuntime;
|
|
42778
42853
|
var init_runtime_mount = __esmMin((() => {
|
|
42779
42854
|
init_connections();
|
|
42780
|
-
init_src$
|
|
42855
|
+
init_src$9();
|
|
42781
42856
|
init_src();
|
|
42782
42857
|
init_registry$1();
|
|
42783
42858
|
init_readiness$1();
|
|
@@ -44912,7 +44987,8 @@ var init_mount = __esmMin((() => {
|
|
|
44912
44987
|
[STACKBONE_ENV_KEYS.s3AccessKey, services.s3AccessKey],
|
|
44913
44988
|
[STACKBONE_ENV_KEYS.s3SecretKey, services.s3SecretKey],
|
|
44914
44989
|
[STACKBONE_ENV_KEYS.s3Bucket, services.s3Bucket],
|
|
44915
|
-
[STACKBONE_ENV_KEYS.s3Region, services.s3Region]
|
|
44990
|
+
[STACKBONE_ENV_KEYS.s3Region, services.s3Region],
|
|
44991
|
+
[STACKBONE_ENV_KEYS.s3ForcePathStyle, String(services.s3ForcePathStyle)]
|
|
44916
44992
|
];
|
|
44917
44993
|
const seeded = [];
|
|
44918
44994
|
for (const [name, value] of wanted) if (env[name] === void 0 || env[name] === "") {
|
|
@@ -44999,8 +45075,8 @@ var init_mount = __esmMin((() => {
|
|
|
44999
45075
|
//#endregion
|
|
45000
45076
|
//#region ../../libs/runtime/workspace-runtime/src/lib/server/emulator-server.ts
|
|
45001
45077
|
init_src$13();
|
|
45002
|
-
init_src$
|
|
45003
|
-
init_src$
|
|
45078
|
+
init_src$11();
|
|
45079
|
+
init_src$9();
|
|
45004
45080
|
init_workflow_guardrails();
|
|
45005
45081
|
init_src$1();
|
|
45006
45082
|
init_recurring_jobs_registry();
|
|
@@ -45021,6 +45097,28 @@ init_well_known();
|
|
|
45021
45097
|
* agents run IN-PROCESS behind it, not on a port of their own.
|
|
45022
45098
|
*/
|
|
45023
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
|
+
};
|
|
45024
45122
|
var buildEmulatorApp = (opts, hooks = {}) => {
|
|
45025
45123
|
const app = new Hono();
|
|
45026
45124
|
const agentBaseUrl = `http://127.0.0.1:${opts.injection.agentPort}`;
|
|
@@ -45043,7 +45141,8 @@ var buildEmulatorApp = (opts, hooks = {}) => {
|
|
|
45043
45141
|
"x-stackbone-session",
|
|
45044
45142
|
"x-stackbone-conversation",
|
|
45045
45143
|
"x-api-key",
|
|
45046
|
-
"anthropic-version"
|
|
45144
|
+
"anthropic-version",
|
|
45145
|
+
STACKBONE_SIGNATURE_HEADER
|
|
45047
45146
|
],
|
|
45048
45147
|
exposeHeaders: ["X-Stackbone-Run-Id"]
|
|
45049
45148
|
}));
|
|
@@ -45053,7 +45152,7 @@ var buildEmulatorApp = (opts, hooks = {}) => {
|
|
|
45053
45152
|
}));
|
|
45054
45153
|
if (opts.proxyChallenge?.secret) {
|
|
45055
45154
|
const challengeSecret = opts.proxyChallenge.secret;
|
|
45056
|
-
app.get(PROXY_CHALLENGE_PATH, (c) => {
|
|
45155
|
+
app.get(PROXY_CHALLENGE_PATH, async (c) => {
|
|
45057
45156
|
const url = new URL(c.req.url);
|
|
45058
45157
|
const ok = verifyStackboneSignature({
|
|
45059
45158
|
secret: challengeSecret,
|
|
@@ -45066,10 +45165,43 @@ var buildEmulatorApp = (opts, hooks = {}) => {
|
|
|
45066
45165
|
});
|
|
45067
45166
|
const body = {
|
|
45068
45167
|
ok,
|
|
45069
|
-
identityVerification: opts.jwtAuth ? "armed" : "
|
|
45168
|
+
identityVerification: !opts.jwtAuth ? "unarmed" : await opts.jwtAuth.answersFor() ? "armed" : "untaught"
|
|
45070
45169
|
};
|
|
45071
45170
|
return c.json(body, ok ? 200 : 401);
|
|
45072
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
|
+
});
|
|
45073
45205
|
}
|
|
45074
45206
|
app.get("/api/contract", (c) => c.json(buildContractResponse()));
|
|
45075
45207
|
app.get("/", (c) => c.html(renderStudioStub({
|
|
@@ -45175,6 +45307,7 @@ var buildEmulatorApp = (opts, hooks = {}) => {
|
|
|
45175
45307
|
secretKey: opts.injection.services.s3SecretKey,
|
|
45176
45308
|
bucket: opts.injection.services.s3Bucket,
|
|
45177
45309
|
region: opts.injection.services.s3Region,
|
|
45310
|
+
forcePathStyle: opts.injection.services.s3ForcePathStyle,
|
|
45178
45311
|
agentId: opts.agent.slug
|
|
45179
45312
|
};
|
|
45180
45313
|
app.route("/api/storage", createStorageRouter(storageOptions));
|
|
@@ -45194,12 +45327,12 @@ var buildEmulatorApp = (opts, hooks = {}) => {
|
|
|
45194
45327
|
invocations: opts.invocations,
|
|
45195
45328
|
...hooks.resolveRagIngestTrigger ? { resolveRagIngestTrigger: hooks.resolveRagIngestTrigger } : {}
|
|
45196
45329
|
}));
|
|
45197
|
-
const secretsOptions = {
|
|
45198
|
-
app.route("/api/secrets", createSecretsRouter(secretsOptions));
|
|
45199
|
-
const privilegedSecretsOptions = {
|
|
45330
|
+
const secretsOptions = {
|
|
45200
45331
|
connectionString: opts.injection.services.postgresUrl,
|
|
45201
45332
|
...opts.secretKey ? { cipherKeyBase64: opts.secretKey } : {}
|
|
45202
45333
|
};
|
|
45334
|
+
app.route("/api/secrets", createSecretsRouter(secretsOptions));
|
|
45335
|
+
const privilegedSecretsOptions = secretsOptions;
|
|
45203
45336
|
app.route("/api/model-provider", createModelProviderRouter(privilegedSecretsOptions, { ...opts.bootEnv ?? {} }, { ...hooks.retryDegradedDeepAgents ? { onProviderSaved: hooks.retryDegradedDeepAgents } : {} }));
|
|
45204
45337
|
app.route("/api/browser-provider", createBrowserProviderRouter(privilegedSecretsOptions));
|
|
45205
45338
|
app.route("/api/browser-profiles", createBrowserProfilesRouter({ connectionString: opts.injection.services.postgresUrl }));
|
|
@@ -45916,7 +46049,8 @@ var buildDevServices = (ports) => ({
|
|
|
45916
46049
|
s3AccessKey: DEV_SERVICE_CREDENTIALS.s3AccessKey,
|
|
45917
46050
|
s3SecretKey: DEV_SERVICE_CREDENTIALS.s3SecretKey,
|
|
45918
46051
|
s3Bucket: DEV_SERVICE_CREDENTIALS.s3Bucket,
|
|
45919
|
-
s3Region: DEV_SERVICE_CREDENTIALS.s3Region
|
|
46052
|
+
s3Region: DEV_SERVICE_CREDENTIALS.s3Region,
|
|
46053
|
+
s3ForcePathStyle: true
|
|
45920
46054
|
});
|
|
45921
46055
|
var buildDevRedisUrl = (redisPort) => `redis://127.0.0.1:${redisPort}`;
|
|
45922
46056
|
var DEV_AGENT_SECRET_KEY = createHash("sha256").update(STUDIO_LOCAL_DEV_ENCRYPTION_KEY).digest("base64");
|
|
@@ -46229,7 +46363,7 @@ var calls_workflow_ts_default = "import { z } from \"zod\";\nimport { streamDeep
|
|
|
46229
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";
|
|
46230
46364
|
//#endregion
|
|
46231
46365
|
//#region src/services/scaffold/template-loader.ts
|
|
46232
|
-
var SDK_TEMPLATE_VERSION = "0.3.
|
|
46366
|
+
var SDK_TEMPLATE_VERSION = "0.3.1";
|
|
46233
46367
|
var PREFIX = "./templates/";
|
|
46234
46368
|
var templates = new Map(Object.entries(/* @__PURE__ */ Object.assign({
|
|
46235
46369
|
"./templates/deep-agents/index.ts.tmpl": index_ts_default,
|
|
@@ -46515,10 +46649,10 @@ var createIdentityTokenProvider = (opts) => {
|
|
|
46515
46649
|
let token = null;
|
|
46516
46650
|
return {
|
|
46517
46651
|
current: () => token,
|
|
46518
|
-
refresh: async () => {
|
|
46652
|
+
refresh: async (target) => {
|
|
46519
46653
|
let minted;
|
|
46520
46654
|
try {
|
|
46521
|
-
minted = await opts.mint();
|
|
46655
|
+
minted = await opts.mint(target);
|
|
46522
46656
|
} catch {
|
|
46523
46657
|
minted = null;
|
|
46524
46658
|
}
|
|
@@ -46530,34 +46664,28 @@ var createIdentityTokenProvider = (opts) => {
|
|
|
46530
46664
|
//#endregion
|
|
46531
46665
|
//#region src/services/identity-token.service.ts
|
|
46532
46666
|
/** Route the control plane mints the box-facing identity JWT on (better-auth jwt plugin). */
|
|
46533
|
-
var IDENTITY_TOKEN_PATH = "/api/auth/token";
|
|
46667
|
+
var IDENTITY_TOKEN_PATH = "/api/auth/token/organization";
|
|
46534
46668
|
/**
|
|
46535
46669
|
* Builds the provider for `ctx`, which MUST be the control-plane context — the
|
|
46536
46670
|
* token is minted by the control plane, never by the box it is presented to.
|
|
46537
|
-
*/
|
|
46538
|
-
var createIdentityTokenService = (ctx) => createIdentityTokenProvider({ mint: async () => {
|
|
46539
|
-
return (await ctx.http.get(IDENTITY_TOKEN_PATH))?.token;
|
|
46540
|
-
} });
|
|
46541
|
-
/**
|
|
46542
|
-
* The organization id a minted identity token carries, or `null` when it carries
|
|
46543
|
-
* none (or is not a readable JWT).
|
|
46544
46671
|
*
|
|
46545
|
-
*
|
|
46546
|
-
*
|
|
46547
|
-
*
|
|
46548
|
-
*
|
|
46549
|
-
*
|
|
46550
|
-
|
|
46551
|
-
|
|
46552
|
-
|
|
46553
|
-
|
|
46554
|
-
|
|
46555
|
-
|
|
46556
|
-
|
|
46557
|
-
|
|
46558
|
-
|
|
46559
|
-
|
|
46560
|
-
|
|
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
|
+
};
|
|
46561
46689
|
};
|
|
46562
46690
|
//#endregion
|
|
46563
46691
|
//#region src/services/organization.service.ts
|
|
@@ -46714,28 +46842,6 @@ var directoryLookupFailed = (installationId, cause) => {
|
|
|
46714
46842
|
});
|
|
46715
46843
|
};
|
|
46716
46844
|
/**
|
|
46717
|
-
* Refuse a box that serves a DIFFERENT organization than the one the minted
|
|
46718
|
-
* identity token names, before a single call is addressed to it.
|
|
46719
|
-
*
|
|
46720
|
-
* `GET /api/auth/token` is session-scoped, not organization-scoped: it mints one
|
|
46721
|
-
* `org` claim per account, the earliest-joined membership. The box hard-rejects a
|
|
46722
|
-
* token whose `org` is not its own tenant, so for an operator who belongs to more
|
|
46723
|
-
* than one organization every box outside that first one answers 401 — and the
|
|
46724
|
-
* retry cannot help, because the re-mint asks the same endpoint and gets the same
|
|
46725
|
-
* token back. Left alone the operator reads "Run `stackbone login`", a remedy
|
|
46726
|
-
* that can never work.
|
|
46727
|
-
*
|
|
46728
|
-
* Only a DEFINITE mismatch refuses. A token with no `org` claim (or one this
|
|
46729
|
-
* build cannot read) falls through to the box's own answer rather than inventing
|
|
46730
|
-
* a second verdict about a shape we do not understand.
|
|
46731
|
-
*/
|
|
46732
|
-
var assertIdentityServesOrg = async (ctx, token, org, installationId) => {
|
|
46733
|
-
const mintedFor = identityTokenOrganizationId(token);
|
|
46734
|
-
if (mintedFor === null || mintedFor === org.id) return;
|
|
46735
|
-
const owner = (await getMyOrganizations(ctx).catch(() => [])).find((o) => o.id === mintedFor);
|
|
46736
|
-
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.");
|
|
46737
|
-
};
|
|
46738
|
-
/**
|
|
46739
46845
|
* Resolves the full studio routing context a surface verb needs: the
|
|
46740
46846
|
* installation it targets (`resolveAgentTarget`) plus the datapath (the
|
|
46741
46847
|
* customer's own box vs the running local emulator) and the effective
|
|
@@ -46827,10 +46933,8 @@ var resolveStudioTarget = async (ctx, flagValue, deps = {}) => {
|
|
|
46827
46933
|
throw directoryLookupFailed(installationId, err);
|
|
46828
46934
|
}
|
|
46829
46935
|
if (!runtimeUrl) return throwRuntimeNotConfigured(installationId);
|
|
46830
|
-
const identity = makeIdentity(ctx);
|
|
46831
|
-
|
|
46832
|
-
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.");
|
|
46833
|
-
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\`.`);
|
|
46834
46938
|
const http = makeAdapter({
|
|
46835
46939
|
apiUrl: runtimeUrl,
|
|
46836
46940
|
tokenStore: ctx.tokenStore,
|
|
@@ -47799,10 +47903,19 @@ var buildWorkspaceBundle = async (options) => {
|
|
|
47799
47903
|
migrationsDir: options.migrationsDir,
|
|
47800
47904
|
outDir
|
|
47801
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
|
+
}
|
|
47802
47915
|
const manifest = deriveWorkspaceManifest({
|
|
47803
47916
|
workspace,
|
|
47804
47917
|
workflowManifest,
|
|
47805
|
-
hasConfigSchema
|
|
47918
|
+
hasConfigSchema,
|
|
47806
47919
|
defaults: options.defaults,
|
|
47807
47920
|
deepAgentExternals
|
|
47808
47921
|
});
|
|
@@ -47864,6 +47977,181 @@ var resolveAgentDbConfig = (agentRoot) => {
|
|
|
47864
47977
|
};
|
|
47865
47978
|
};
|
|
47866
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
|
|
47867
48155
|
//#region src/dev/detect-workflow-streaming.ts
|
|
47868
48156
|
/** `getWritable` imported from the literal `'workflow'` package (single or
|
|
47869
48157
|
* double quotes, other named imports around it allowed). */
|
|
@@ -47933,7 +48221,7 @@ var extractWorkflowSchemas = async (projectRoot, workflows, opts) => {
|
|
|
47933
48221
|
schemas[w.name] = { ...NULL_PAIR };
|
|
47934
48222
|
schedules[w.name] = [];
|
|
47935
48223
|
}
|
|
47936
|
-
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)}`);
|
|
47937
48225
|
return {
|
|
47938
48226
|
schemas,
|
|
47939
48227
|
schedules,
|
|
@@ -47996,11 +48284,11 @@ var convertHalf = (workflowName, half, schema, toJsonSchema, emit) => {
|
|
|
47996
48284
|
try {
|
|
47997
48285
|
return toJsonSchema(schema, half);
|
|
47998
48286
|
} catch (err) {
|
|
47999
|
-
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)}`);
|
|
48000
48288
|
return null;
|
|
48001
48289
|
}
|
|
48002
48290
|
};
|
|
48003
|
-
var errMessage$
|
|
48291
|
+
var errMessage$1 = (err) => err instanceof Error ? err.message : String(err);
|
|
48004
48292
|
/**
|
|
48005
48293
|
* The default loader: generate an entry that re-exports ONLY each workflow's
|
|
48006
48294
|
* `inputSchema`/`outputSchema` (resolved from the workflow module or a
|
|
@@ -48022,9 +48310,9 @@ var defaultWorkflowSchemaLoader = async (projectRoot, workflows, outDir) => {
|
|
|
48022
48310
|
try {
|
|
48023
48311
|
esbuild = await import(pathToFileURL(projectRequire.resolve("esbuild")).href);
|
|
48024
48312
|
} catch (err) {
|
|
48025
|
-
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)}`);
|
|
48026
48314
|
}
|
|
48027
|
-
const entry = await buildEntrySource
|
|
48315
|
+
const entry = await buildEntrySource(projectRoot, workflows);
|
|
48028
48316
|
const outfile = resolve(outDir, WORKFLOW_WELL_KNOWN_DIR, WORKFLOW_SCHEMAS_FILE);
|
|
48029
48317
|
await esbuild.build({
|
|
48030
48318
|
stdin: {
|
|
@@ -48059,7 +48347,7 @@ var defaultWorkflowSchemaLoader = async (projectRoot, workflows, outDir) => {
|
|
|
48059
48347
|
* Schema source per workflow: a `<base>.contract.ts` sibling when it exists (the
|
|
48060
48348
|
* clean variant — only zod, no closure), else the workflow module itself.
|
|
48061
48349
|
*/
|
|
48062
|
-
var buildEntrySource
|
|
48350
|
+
var buildEntrySource = async (projectRoot, workflows) => {
|
|
48063
48351
|
const lines = [
|
|
48064
48352
|
`import { z } from 'zod';`,
|
|
48065
48353
|
`// Non-Zod Standard Schema fallback. Per @standard-schema/spec, ~standard`,
|
|
@@ -48709,7 +48997,7 @@ var withEntry = (workflows, seed) => {
|
|
|
48709
48997
|
export: seed.export
|
|
48710
48998
|
}].sort((a, b) => a.name.localeCompare(b.name));
|
|
48711
48999
|
};
|
|
48712
|
-
var errMessage
|
|
49000
|
+
var errMessage = (err) => err instanceof Error ? err.message : String(err);
|
|
48713
49001
|
/**
|
|
48714
49002
|
* Resolve both platform-managed workflows and return the workspace's workflow
|
|
48715
49003
|
* list with them spliced in, ready for the compiler (which already knows how to
|
|
@@ -48725,14 +49013,14 @@ var seedManagedWorkflows = async (projectRoot, workflows, logger, deps = {}) =>
|
|
|
48725
49013
|
logger.info(`rag-ingest workflow: using the ${seed.active === "default" ? "platform default" : "creator override"} (${seed.module})`);
|
|
48726
49014
|
result = withEntry(result, seed);
|
|
48727
49015
|
} catch (err) {
|
|
48728
|
-
logger.warn({ err: errMessage
|
|
49016
|
+
logger.warn({ err: errMessage(err) }, "failed to seed the default rag-ingest workflow — RAG upload ingest may be unavailable");
|
|
48729
49017
|
}
|
|
48730
49018
|
try {
|
|
48731
49019
|
const seed = await seedMappingSuggest(projectRoot);
|
|
48732
49020
|
logger.info(`mapping-suggest workflow: using the ${seed.active === "default" ? "platform default" : "creator override"} (${seed.module})`);
|
|
48733
49021
|
result = withEntry(result, seed);
|
|
48734
49022
|
} catch (err) {
|
|
48735
|
-
logger.warn({ err: errMessage
|
|
49023
|
+
logger.warn({ err: errMessage(err) }, "failed to seed the default mapping-suggest workflow — Auto-map may be unavailable");
|
|
48736
49024
|
}
|
|
48737
49025
|
return resolveSystemWorkflows(result);
|
|
48738
49026
|
};
|
|
@@ -48969,6 +49257,7 @@ var buildWorkspace = async (ctx, outDirArg, deps = {}) => {
|
|
|
48969
49257
|
const discover = deps.discoverWorkspace ?? discoverWorkspace;
|
|
48970
49258
|
const build = deps.buildWorkspaceBundle ?? buildWorkspaceBundle;
|
|
48971
49259
|
const seedManaged = deps.seedManagedWorkflows ?? seedManagedWorkflows;
|
|
49260
|
+
const extractSchema = deps.extractConfigSchema ?? extractConfigSchema;
|
|
48972
49261
|
const projectRoot = ctx.cwd;
|
|
48973
49262
|
const outDir = resolve(projectRoot, outDirArg ?? "dist/workspace-bundle");
|
|
48974
49263
|
const workspace = await discover(projectRoot);
|
|
@@ -48991,6 +49280,7 @@ var buildWorkspace = async (ctx, outDirArg, deps = {}) => {
|
|
|
48991
49280
|
workflows: workspace.workflows,
|
|
48992
49281
|
logger: { warn: (msg) => ctx.logger.warn(msg) }
|
|
48993
49282
|
}),
|
|
49283
|
+
resolveConfigSchema: () => extractSchema(projectRoot, { logger: { warn: (msg) => ctx.logger.warn(msg) } }).then((result) => result.schema),
|
|
48994
49284
|
logger: {
|
|
48995
49285
|
info: (msg) => ctx.logger.info(msg),
|
|
48996
49286
|
warn: (msg) => ctx.logger.warn(msg)
|
|
@@ -49067,8 +49357,48 @@ var RUNTIME_PORT = 8080;
|
|
|
49067
49357
|
var POSTGRES_IMAGE$1 = "pgvector/pgvector:pg17";
|
|
49068
49358
|
/** Redis image the generated compose runs (BullMQ backs the durable World). */
|
|
49069
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;
|
|
49070
49391
|
/** A fresh 32-byte key, base64 — the format `STACKBONE_SECRET_KEY` expects. */
|
|
49071
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");
|
|
49072
49402
|
var renderDockerfile = (input) => `# Generated by \`stackbone package\`. Two lines is the whole build: the Stackbone
|
|
49073
49403
|
# runtime base already carries the server and its pinned dependencies, so this
|
|
49074
49404
|
# image adds only your workspace. No monorepo, no npm install, seconds to build.
|
|
@@ -49077,10 +49407,10 @@ COPY ${input.workspaceDirName}/ /app/workspace/
|
|
|
49077
49407
|
`;
|
|
49078
49408
|
var renderCompose = (input) => {
|
|
49079
49409
|
const image = input.imageTag;
|
|
49080
|
-
return `# Generated by \`stackbone package\`. Brings up the agent with a Postgres
|
|
49081
|
-
# Redis of its own. If you already run those, delete
|
|
49082
|
-
#
|
|
49083
|
-
# 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.
|
|
49084
49414
|
|
|
49085
49415
|
services:
|
|
49086
49416
|
postgres:
|
|
@@ -49112,6 +49442,45 @@ services:
|
|
|
49112
49442
|
retries: 20
|
|
49113
49443
|
restart: unless-stopped
|
|
49114
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
|
+
|
|
49115
49484
|
agent:
|
|
49116
49485
|
${input.imageArchiveName !== void 0 ? ` # Loaded from ${input.imageArchiveName} — there is nothing to build here.
|
|
49117
49486
|
image: ${image}` : ` build:
|
|
@@ -49121,9 +49490,17 @@ ${input.imageArchiveName !== void 0 ? ` # Loaded from ${input.imageArchiveNam
|
|
|
49121
49490
|
env_file:
|
|
49122
49491
|
- .env
|
|
49123
49492
|
environment:
|
|
49124
|
-
# Point the agent at the
|
|
49493
|
+
# Point the agent at the three services above. Override in .env to use your own.
|
|
49125
49494
|
DATABASE_URL: \${DATABASE_URL:-postgres://stackbone:\${POSTGRES_PASSWORD:-stackbone}@postgres:5432/stackbone}
|
|
49126
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}
|
|
49127
49504
|
ports:
|
|
49128
49505
|
# The agent serves on ${RUNTIME_PORT} inside the container. Change the LEFT
|
|
49129
49506
|
# number to publish it elsewhere on the host.
|
|
@@ -49133,25 +49510,51 @@ ${input.imageArchiveName !== void 0 ? ` # Loaded from ${input.imageArchiveNam
|
|
|
49133
49510
|
condition: service_healthy
|
|
49134
49511
|
redis:
|
|
49135
49512
|
condition: service_healthy
|
|
49513
|
+
minio-create-bucket:
|
|
49514
|
+
condition: service_completed_successfully
|
|
49136
49515
|
restart: unless-stopped
|
|
49137
49516
|
|
|
49138
49517
|
volumes:
|
|
49139
49518
|
agent-postgres:
|
|
49140
49519
|
agent-redis:
|
|
49520
|
+
agent-objects:
|
|
49141
49521
|
`;
|
|
49142
49522
|
};
|
|
49143
|
-
|
|
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}.
|
|
49535
|
+
#
|
|
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}.
|
|
49144
49539
|
#
|
|
49145
|
-
#
|
|
49146
|
-
#
|
|
49147
|
-
#
|
|
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.`}
|
|
49148
49544
|
|
|
49149
49545
|
# ---- Keep this safe ----------------------------------------------------------
|
|
49150
49546
|
# Every secret this agent stores in its database is encrypted with this key. It
|
|
49151
49547
|
# cannot live in the database it protects. LOSE IT AND YOU LOSE THOSE SECRETS —
|
|
49152
49548
|
# there is no recovery, they have to be entered again. Changing it has the same
|
|
49153
49549
|
# effect as losing it.
|
|
49154
|
-
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=`}
|
|
49155
49558
|
|
|
49156
49559
|
# ---- Your infrastructure (optional here) -------------------------------------
|
|
49157
49560
|
# Uncomment to point the agent at a Postgres and a Redis you already run, instead
|
|
@@ -49159,7 +49562,31 @@ STACKBONE_SECRET_KEY=${secretKey}
|
|
|
49159
49562
|
# agent creates its own schema on first boot. It must have the pgvector extension
|
|
49160
49563
|
# available — see the README.
|
|
49161
49564
|
# DATABASE_URL=postgres://user:password@host:5432/dbname?sslmode=require
|
|
49162
|
-
# WORKFLOW_REDIS_URL=redis://host:6379
|
|
49565
|
+
# WORKFLOW_REDIS_URL=redis://default:password@host:6379
|
|
49566
|
+
# Managed Redis always wants a password, and some require TLS — use rediss:// for
|
|
49567
|
+
# those. Percent-encode a password containing / + = or @, or the URL will not parse.
|
|
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
|
|
49163
49590
|
|
|
49164
49591
|
# ---- Optional ----------------------------------------------------------------
|
|
49165
49592
|
# Set if your database role is not allowed to create schemas; you then apply the
|
|
@@ -49167,13 +49594,19 @@ STACKBONE_SECRET_KEY=${secretKey}
|
|
|
49167
49594
|
# where it lives.
|
|
49168
49595
|
# STACKBONE_SKIP_MIGRATIONS=1
|
|
49169
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);
|
|
49170
49601
|
var renderInventory = (label, names) => names.length === 0 ? `No ${label}.` : names.map((n) => `- \`${n}\``).join("\n");
|
|
49171
49602
|
var renderReadme = (input) => {
|
|
49172
49603
|
const offline = input.imageArchiveName;
|
|
49173
49604
|
return `# ${input.agentName}
|
|
49174
49605
|
|
|
49175
|
-
A Stackbone agent, packaged to run in your own environment. It talks to a Postgres
|
|
49176
|
-
|
|
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\`.
|
|
49177
49610
|
|
|
49178
49611
|
## What you are running
|
|
49179
49612
|
|
|
@@ -49214,10 +49647,36 @@ prints it again. You need that value only if you later connect this deployment t
|
|
|
49214
49647
|
a Stackbone control plane. Copy it somewhere safe now if there is any chance you
|
|
49215
49648
|
will. Anyone who can read these logs can read it too.
|
|
49216
49649
|
|
|
49217
|
-
##
|
|
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:
|
|
49218
49665
|
|
|
49219
|
-
|
|
49220
|
-
|
|
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
|
|
49675
|
+
|
|
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.
|
|
49221
49680
|
|
|
49222
49681
|
**The database has one requirement: the \`pgvector\` extension must be available.**
|
|
49223
49682
|
The agent runs \`CREATE EXTENSION IF NOT EXISTS vector\` while creating its schema.
|
|
@@ -49244,6 +49703,32 @@ and apply these by hand first, in filename order:
|
|
|
49244
49703
|
|
|
49245
49704
|
Both live inside the image; \`docker run --rm ${input.imageTag} ls <path>\` lists them.
|
|
49246
49705
|
|
|
49706
|
+
Redis needs no preparation, only a reachable URL. Managed instances hand out a
|
|
49707
|
+
password, so the URL carries one — \`redis://default:password@host:6379\`. Use
|
|
49708
|
+
\`rediss://\` where the provider requires TLS (Upstash, ElastiCache with encryption
|
|
49709
|
+
in transit), and percent-encode a password containing \`/\`, \`+\`, \`=\` or \`@\`. The
|
|
49710
|
+
agent PINGs Redis before it starts and stops with the reason if it cannot: a
|
|
49711
|
+
rejected password fails right away, and a Redis still starting up is retried for
|
|
49712
|
+
30 seconds first.
|
|
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
|
+
|
|
49247
49732
|
## Running it somewhere other than Compose
|
|
49248
49733
|
|
|
49249
49734
|
The agent serves on port **${RUNTIME_PORT}** by default, and binds whatever
|
|
@@ -49335,6 +49820,7 @@ var runPackage = async (ctx, args, deps = {}) => {
|
|
|
49335
49820
|
const build = deps.buildWorkspace ?? buildWorkspace;
|
|
49336
49821
|
const docker = deps.runDocker ?? runDockerCommand;
|
|
49337
49822
|
const makeKey = deps.generateSecretKey ?? generateSecretKey;
|
|
49823
|
+
const makeStoragePassword = deps.generateStoragePassword ?? generateStoragePassword;
|
|
49338
49824
|
const projectRoot = ctx.cwd;
|
|
49339
49825
|
const outDir = resolve(projectRoot, args["out-dir"] ?? "dist/deploy");
|
|
49340
49826
|
const { bundle } = await build(ctx, void 0, deps);
|
|
@@ -49354,16 +49840,21 @@ var runPackage = async (ctx, args, deps = {}) => {
|
|
|
49354
49840
|
recursive: true,
|
|
49355
49841
|
force: true
|
|
49356
49842
|
});
|
|
49357
|
-
const
|
|
49843
|
+
const secrets = {
|
|
49844
|
+
secretKey: makeKey(),
|
|
49845
|
+
storagePassword: makeStoragePassword()
|
|
49846
|
+
};
|
|
49358
49847
|
const written = [
|
|
49359
49848
|
"Dockerfile",
|
|
49360
49849
|
"docker-compose.yml",
|
|
49361
49850
|
".env",
|
|
49851
|
+
".env.example",
|
|
49362
49852
|
"README.md"
|
|
49363
49853
|
];
|
|
49364
49854
|
await writeFile(join(outDir, "Dockerfile"), renderDockerfile(templateInput), "utf8");
|
|
49365
49855
|
await writeFile(join(outDir, "docker-compose.yml"), renderCompose(templateInput), "utf8");
|
|
49366
|
-
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");
|
|
49367
49858
|
await writeFile(join(outDir, "README.md"), renderReadme(templateInput), "utf8");
|
|
49368
49859
|
if (args.offline) {
|
|
49369
49860
|
ctx.logger.info(`Building ${imageTag} and saving it to ${IMAGE_ARCHIVE_NAME} (~150 MB)`);
|
|
@@ -49392,9 +49883,11 @@ var runPackage = async (ctx, args, deps = {}) => {
|
|
|
49392
49883
|
...degraded.length > 0 ? [` degraded ${degraded.map((d) => d.name).join(", ")} — shipped, but they will not run`] : [],
|
|
49393
49884
|
` serves on ${RUNTIME_PORT} (or PORT, when the host assigns one)`,
|
|
49394
49885
|
"",
|
|
49395
|
-
"The .env holds
|
|
49396
|
-
"
|
|
49397
|
-
"
|
|
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.",
|
|
49398
49891
|
...args.offline ? [] : [
|
|
49399
49892
|
"",
|
|
49400
49893
|
`Whoever runs this needs to reach ${templateInput.baseImage} once, to pull it.`,
|
|
@@ -49440,181 +49933,6 @@ var createPackageCommand = defineVerb({
|
|
|
49440
49933
|
handler: ({ ctx, args }) => runPackage(ctx, args)
|
|
49441
49934
|
});
|
|
49442
49935
|
//#endregion
|
|
49443
|
-
//#region src/dev/extract-config-schema.ts
|
|
49444
|
-
/** Directory the extracted artifact lives in, relative to the workspace root.
|
|
49445
|
-
* A subdir of its own, parallel to the workflow well-known tree. */
|
|
49446
|
-
var CONFIG_WELL_KNOWN_DIR = ".well-known/config/v1";
|
|
49447
|
-
/** The shipped artifact: the single JSON Schema (JSON only, no live sidecar). */
|
|
49448
|
-
var CONFIG_SCHEMA_FILE = "config-schema.json";
|
|
49449
|
-
/** The intermediate esbuild bundle (harmless to leave behind, like the workflow
|
|
49450
|
-
* sidecar). NOT the deliverable — `config-schema.json` is. */
|
|
49451
|
-
var CONFIG_SCHEMA_BUNDLE = "config-schema.mjs";
|
|
49452
|
-
/**
|
|
49453
|
-
* Extract the workspace's single config JSON Schema. Defensive by contract: an
|
|
49454
|
-
* absent `config.schema.ts` degrades to `null` SILENTLY (the editor stays
|
|
49455
|
-
* free-form); a DECLARED-but-inconvertible schema degrades to `null` plus one
|
|
49456
|
-
* noisy warning; and a total bundle failure degrades to `null` plus one coarse
|
|
49457
|
-
* warning. It NEVER throws — a broken config schema must not break the dev build.
|
|
49458
|
-
*/
|
|
49459
|
-
var extractConfigSchema = async (projectRoot, opts = {}) => {
|
|
49460
|
-
const loader = opts.loader ?? defaultConfigSchemaLoader;
|
|
49461
|
-
const warnings = [];
|
|
49462
|
-
const emit = (msg) => {
|
|
49463
|
-
warnings.push(msg);
|
|
49464
|
-
opts.logger?.warn(msg);
|
|
49465
|
-
};
|
|
49466
|
-
let loaded;
|
|
49467
|
-
try {
|
|
49468
|
-
loaded = await loader(projectRoot);
|
|
49469
|
-
} catch (err) {
|
|
49470
|
-
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)}`);
|
|
49471
|
-
return {
|
|
49472
|
-
schema: null,
|
|
49473
|
-
warnings
|
|
49474
|
-
};
|
|
49475
|
-
}
|
|
49476
|
-
if (loaded.configSchema == null) return {
|
|
49477
|
-
schema: null,
|
|
49478
|
-
warnings
|
|
49479
|
-
};
|
|
49480
|
-
try {
|
|
49481
|
-
return {
|
|
49482
|
-
schema: loaded.toJsonSchema(loaded.configSchema),
|
|
49483
|
-
warnings
|
|
49484
|
-
};
|
|
49485
|
-
} catch (err) {
|
|
49486
|
-
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)}`);
|
|
49487
|
-
return {
|
|
49488
|
-
schema: null,
|
|
49489
|
-
warnings
|
|
49490
|
-
};
|
|
49491
|
-
}
|
|
49492
|
-
};
|
|
49493
|
-
/**
|
|
49494
|
-
* Extract the config schema and WRITE the artifact. When the schema is non-null,
|
|
49495
|
-
* writes `<projectRoot>/.well-known/config/v1/config-schema.json` (compact JSON,
|
|
49496
|
-
* like `manifest.json`) and returns `written: true`. When the schema is null
|
|
49497
|
-
* (absent or inconvertible), REMOVES any stale `config-schema.json` so a deleted
|
|
49498
|
-
* or broken schema does not leave a phantom artifact, and returns `written:
|
|
49499
|
-
* false`. Never throws (mirrors `extractConfigSchema`).
|
|
49500
|
-
*/
|
|
49501
|
-
var buildConfigSchema = async (projectRoot, opts = {}) => {
|
|
49502
|
-
const { schema, warnings } = await extractConfigSchema(projectRoot, opts);
|
|
49503
|
-
const outfile = resolve(projectRoot, CONFIG_WELL_KNOWN_DIR, CONFIG_SCHEMA_FILE);
|
|
49504
|
-
if (schema == null) {
|
|
49505
|
-
await rm(outfile, { force: true });
|
|
49506
|
-
return {
|
|
49507
|
-
schema: null,
|
|
49508
|
-
warnings,
|
|
49509
|
-
written: false
|
|
49510
|
-
};
|
|
49511
|
-
}
|
|
49512
|
-
await mkdir(resolve(projectRoot, CONFIG_WELL_KNOWN_DIR), { recursive: true });
|
|
49513
|
-
await writeFile(outfile, JSON.stringify(schema), "utf8");
|
|
49514
|
-
return {
|
|
49515
|
-
schema,
|
|
49516
|
-
warnings,
|
|
49517
|
-
written: true
|
|
49518
|
-
};
|
|
49519
|
-
};
|
|
49520
|
-
var errMessage = (err) => err instanceof Error ? err.message : String(err);
|
|
49521
|
-
/**
|
|
49522
|
-
* The default loader: when `config.schema.ts` exists at the project root, emit an
|
|
49523
|
-
* entry that re-exports ONLY its `configSchema` (static member access for
|
|
49524
|
-
* tree-shaking), esbuild-bundle it to
|
|
49525
|
-
* `<projectRoot>/.well-known/config/v1/config-schema.mjs`, import it back, and
|
|
49526
|
-
* read `configSchema` + `toJsonSchema`. When the file is ABSENT, return
|
|
49527
|
-
* `{ configSchema: null, toJsonSchema: () => null }` WITHOUT bundling — an absent
|
|
49528
|
-
* schema is silent, not a warning.
|
|
49529
|
-
*
|
|
49530
|
-
* Resolved FROM THE PROJECT: esbuild + zod come from the workspace's own
|
|
49531
|
-
* node_modules (like `buildWorkflows` resolves `@workflow/builders`), so the
|
|
49532
|
-
* bundle uses the SAME zod the schema was authored with and the heavy esbuild
|
|
49533
|
-
* tree stays out of the published CLI bundle.
|
|
49534
|
-
*/
|
|
49535
|
-
var defaultConfigSchemaLoader = async (projectRoot) => {
|
|
49536
|
-
try {
|
|
49537
|
-
await access(resolve(projectRoot, CONFIG_SCHEMA_SOURCE));
|
|
49538
|
-
} catch {
|
|
49539
|
-
return {
|
|
49540
|
-
configSchema: null,
|
|
49541
|
-
toJsonSchema: () => null
|
|
49542
|
-
};
|
|
49543
|
-
}
|
|
49544
|
-
const projectRequire = createRequire(resolve(projectRoot, "package.json"));
|
|
49545
|
-
let esbuild;
|
|
49546
|
-
try {
|
|
49547
|
-
esbuild = await import(pathToFileURL(projectRequire.resolve("esbuild")).href);
|
|
49548
|
-
} catch (err) {
|
|
49549
|
-
throw new Error(`Cannot resolve esbuild from the workspace (needed to extract the config schema). Cause: ${errMessage(err)}`);
|
|
49550
|
-
}
|
|
49551
|
-
const entry = buildEntrySource();
|
|
49552
|
-
const outfile = resolve(projectRoot, CONFIG_WELL_KNOWN_DIR, CONFIG_SCHEMA_BUNDLE);
|
|
49553
|
-
await esbuild.build({
|
|
49554
|
-
stdin: {
|
|
49555
|
-
contents: entry,
|
|
49556
|
-
resolveDir: projectRoot,
|
|
49557
|
-
sourcefile: "config-schema-entry.ts",
|
|
49558
|
-
loader: "ts"
|
|
49559
|
-
},
|
|
49560
|
-
bundle: true,
|
|
49561
|
-
packages: "external",
|
|
49562
|
-
format: "esm",
|
|
49563
|
-
platform: "node",
|
|
49564
|
-
target: "node22",
|
|
49565
|
-
treeShaking: true,
|
|
49566
|
-
outfile,
|
|
49567
|
-
logLevel: "silent"
|
|
49568
|
-
});
|
|
49569
|
-
const mod = await import(`${pathToFileURL(outfile).href}?v=${Date.now()}`);
|
|
49570
|
-
if (typeof mod.toJsonSchema !== "function") throw new Error(`config-schema.mjs did not export the expected { configSchema, toJsonSchema } shape.`);
|
|
49571
|
-
return {
|
|
49572
|
-
configSchema: mod.configSchema ?? null,
|
|
49573
|
-
toJsonSchema: mod.toJsonSchema
|
|
49574
|
-
};
|
|
49575
|
-
};
|
|
49576
|
-
/**
|
|
49577
|
-
* Generate the bundle entry source. It NAMESPACE-imports `config.schema.ts` and
|
|
49578
|
-
* reads `configSchema` with DIRECT static member access — so esbuild tree-shakes
|
|
49579
|
-
* everything else and a MISSING export is simply `undefined` (→ `null`), never a
|
|
49580
|
-
* bundle-breaking "no matching export".
|
|
49581
|
-
*
|
|
49582
|
-
* The converter is REPLICATED here (not imported from the workflow extractor): it
|
|
49583
|
-
* runs inside the emitted `.mjs` with the PROJECT's own zod. It is byte-for-byte
|
|
49584
|
-
* the workflow extractor's converter MINUS the `half` parameter — config is a
|
|
49585
|
-
* single input-shaped object, so the Standard-Schema fallback invokes the
|
|
49586
|
-
* `.input` converter directly.
|
|
49587
|
-
*/
|
|
49588
|
-
var buildEntrySource = () => {
|
|
49589
|
-
return [
|
|
49590
|
-
`import { z } from 'zod';`,
|
|
49591
|
-
`import * as cfg from './config.schema.ts';`,
|
|
49592
|
-
`// Non-Zod Standard Schema fallback. Per @standard-schema/spec, ~standard`,
|
|
49593
|
-
`// .jsonSchema is a Converter { input(opts), output(opts) } of FUNCTIONS — so`,
|
|
49594
|
-
`// we INVOKE the input converter to get the actual JSON Schema (config is an`,
|
|
49595
|
-
`// input-shaped object). Returning the Converter object verbatim folds`,
|
|
49596
|
-
`// non-serialisable functions that JSON.stringify drops → a phantom {} schema.`,
|
|
49597
|
-
`const std = (s) => {`,
|
|
49598
|
-
` const c = s && s['~standard'] && s['~standard'].jsonSchema;`,
|
|
49599
|
-
` const fn = c && c.input;`,
|
|
49600
|
-
` return typeof fn === 'function' ? fn({ target: 'draft-2020-12' }) : null;`,
|
|
49601
|
-
`};`,
|
|
49602
|
-
`const carriesFns = (o) =>`,
|
|
49603
|
-
` o != null && typeof o === 'object' && Object.values(o).some((v) => typeof v === 'function');`,
|
|
49604
|
-
`export const toJsonSchema = (s) => {`,
|
|
49605
|
-
` if (s == null) return null;`,
|
|
49606
|
-
` let out;`,
|
|
49607
|
-
` try { out = z.toJSONSchema(s); }`,
|
|
49608
|
-
` catch (e) { out = std(s); if (out == null) throw e; }`,
|
|
49609
|
-
` // A result carrying functions is a non-serialisable phantom; degrade to`,
|
|
49610
|
-
` // null + a noisy warning instead of writing garbage to the artifact.`,
|
|
49611
|
-
` if (carriesFns(out)) throw new Error('schema is non-serialisable (carries functions)');`,
|
|
49612
|
-
` return out;`,
|
|
49613
|
-
`};`,
|
|
49614
|
-
`export const configSchema = cfg.configSchema ?? null;`
|
|
49615
|
-
].join("\n");
|
|
49616
|
-
};
|
|
49617
|
-
//#endregion
|
|
49618
49936
|
//#region src/dev/generate-config-types.ts
|
|
49619
49937
|
/** Path of the generated artifact relative to the agent root. */
|
|
49620
49938
|
var CONFIG_TYPES_RELATIVE_PATH = join(".stackbone", "config.d.ts");
|
|
@@ -54060,7 +54378,7 @@ var buildHandler$2 = (ctx, flags) => protectedCommand(async (innerCtx) => {
|
|
|
54060
54378
|
tunnelUrl: session.publicUrl,
|
|
54061
54379
|
localUrl: session.emulatorUrl,
|
|
54062
54380
|
deeplink,
|
|
54063
|
-
contractVersion:
|
|
54381
|
+
contractVersion: 15,
|
|
54064
54382
|
capabilityCount: EMULATOR_CAPABILITIES.length,
|
|
54065
54383
|
agentName: agent.name,
|
|
54066
54384
|
colors: process.stdout.isTTY === true
|
|
@@ -55386,9 +55704,10 @@ new version means deploying a new image and re-running \`link\`.
|
|
|
55386
55704
|
|
|
55387
55705
|
Turn this project into a folder someone else can run, with no access to
|
|
55388
55706
|
Stackbone and no knowledge of it. The folder holds your compiled workspace and
|
|
55389
|
-
|
|
55390
|
-
brings up a Postgres and
|
|
55391
|
-
\`
|
|
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.
|
|
55392
55711
|
|
|
55393
55712
|
\`\`\`sh
|
|
55394
55713
|
stackbone package # writes dist/deploy
|
|
@@ -55423,9 +55742,10 @@ What lands in the output directory:
|
|
|
55423
55742
|
dist/deploy/
|
|
55424
55743
|
workspace/ # the compiled bundle, exactly what \`stackbone build\` writes
|
|
55425
55744
|
Dockerfile # FROM the published runtime base, COPY workspace/
|
|
55426
|
-
docker-compose.yml # the agent, a Postgres with pgvector, a Redis
|
|
55427
|
-
.env #
|
|
55428
|
-
|
|
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
|
|
55429
55749
|
agent-image.tar # only with --offline
|
|
55430
55750
|
\`\`\`
|
|
55431
55751
|
|
|
@@ -55442,8 +55762,9 @@ to build. That variant needs Docker installed and running where you package, and
|
|
|
55442
55762
|
it emits no \`build:\` block in the compose file: building is the thing the flag
|
|
55443
55763
|
exists to avoid.
|
|
55444
55764
|
|
|
55445
|
-
**
|
|
55446
|
-
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.
|
|
55447
55768
|
|
|
55448
55769
|
The database needs the **\`pgvector\` extension available**. The first boot runs
|
|
55449
55770
|
\`CREATE EXTENSION IF NOT EXISTS vector\` while creating its schema, so a stock
|
|
@@ -55453,6 +55774,16 @@ Postgres applies most of the schema and then stops with
|
|
|
55453
55774
|
of your own and the extension has to be available there, which on many managed
|
|
55454
55775
|
databases only an administrator can create.
|
|
55455
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
|
+
|
|
55456
55787
|
**The agent serves on port 8080, or on \`PORT\` when the host sets one.** Hosts
|
|
55457
55788
|
that assign a port and expect the application to adopt it (Cloud Run, Railway,
|
|
55458
55789
|
Heroku, App Runner) need no extra configuration. Point their health check at
|
|
@@ -55460,9 +55791,23 @@ Heroku, App Runner) need no extra configuration. Point their health check at
|
|
|
55460
55791
|
while \`/health\` also probes the database, which the very first boot is still
|
|
55461
55792
|
creating.
|
|
55462
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
|
+
|
|
55463
55804
|
**Hand the folder over intact.** The generated \`.env\` holds the only copy of
|
|
55464
55805
|
\`STACKBONE_SECRET_KEY\`, and everything the agent stores is encrypted with it.
|
|
55465
|
-
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.
|
|
55466
55811
|
|
|
55467
55812
|
**JSON payload**
|
|
55468
55813
|
|
|
@@ -55470,7 +55815,7 @@ Losing it or changing it makes those secrets unreadable, with no recovery.
|
|
|
55470
55815
|
{
|
|
55471
55816
|
"schema_version": 1,
|
|
55472
55817
|
"outDir": "/abs/path/dist/deploy",
|
|
55473
|
-
"files": ["Dockerfile", "docker-compose.yml", ".env", "README.md"],
|
|
55818
|
+
"files": ["Dockerfile", "docker-compose.yml", ".env", ".env.example", "README.md"],
|
|
55474
55819
|
"imageTag": "acme-agent:latest",
|
|
55475
55820
|
"baseImage": "ghcr.io/stackbone/stackbone-workspace-runtime:node24-base",
|
|
55476
55821
|
"workflows": ["onboarding", "rag-ingest", "mapping-suggest"],
|
|
@@ -55504,6 +55849,7 @@ What lands in the output directory:
|
|
|
55504
55849
|
\`\`\`text
|
|
55505
55850
|
dist/workspace-bundle/
|
|
55506
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
|
|
55507
55853
|
.well-known/workflow/v1/step.mjs # your compiled workflow steps
|
|
55508
55854
|
.well-known/workflow/v1/workflow.vm.js # the workflow bundle the VM runs
|
|
55509
55855
|
.well-known/workflow/v1/manifest.json # workflow ids, schemas, schedules
|
|
@@ -55523,6 +55869,14 @@ the container logs a warning naming the file and starts runs **unvalidated** —
|
|
|
55523
55869
|
a start that your laptop rejects would be accepted in production. The warning is
|
|
55524
55870
|
the thing to grep for after a deploy.
|
|
55525
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
|
+
|
|
55526
55880
|
Each deep agent is bundled with its own dependencies inlined, except for the
|
|
55527
55881
|
packages that must stay a single copy per process. If one of your dependencies
|
|
55528
55882
|
cannot survive bundling, keep it out with \`build.external\`. See
|
|
@@ -56808,19 +57162,20 @@ datastores locally as in production**. You import that client directly
|
|
|
56808
57162
|
inside a tool's execution or a workflow \`'use step'\`; there is no handler
|
|
56809
57163
|
wiring to configure.
|
|
56810
57164
|
|
|
56811
|
-
| Variable
|
|
56812
|
-
|
|
|
56813
|
-
| \`PORT\`
|
|
56814
|
-
| \`STACKBONE_POSTGRES_URL\`
|
|
56815
|
-
| \`STACKBONE_S3_ENDPOINT\`
|
|
56816
|
-
| \`STACKBONE_S3_BUCKET\`
|
|
56817
|
-
| \`STACKBONE_S3_ACCESS_KEY\`
|
|
56818
|
-
| \`STACKBONE_S3_SECRET_KEY\`
|
|
56819
|
-
| \`STACKBONE_S3_REGION\`
|
|
56820
|
-
| \`
|
|
56821
|
-
| \`
|
|
56822
|
-
| \`
|
|
56823
|
-
| \`
|
|
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. |
|
|
56824
57179
|
|
|
56825
57180
|
> See [Configuration → Environment variables](/docs/cli/configuration#environment-variables)
|
|
56826
57181
|
> for the full list.
|
|
@@ -58823,14 +59178,31 @@ A deployed agent never configures storage by hand. The runtime injects these
|
|
|
58823
59178
|
environment variables when your container boots, and the ambient \`stackbone\`
|
|
58824
59179
|
client reads them on first use:
|
|
58825
59180
|
|
|
58826
|
-
| Env var the runtime injects
|
|
58827
|
-
|
|
|
58828
|
-
| \`STACKBONE_S3_ACCESS_KEY\`
|
|
58829
|
-
| \`STACKBONE_S3_SECRET_KEY\`
|
|
58830
|
-
| \`STACKBONE_S3_ENDPOINT\`
|
|
58831
|
-
| \`STACKBONE_S3_BUCKET\`
|
|
58832
|
-
| \`STACKBONE_S3_REGION\`
|
|
58833
|
-
| \`
|
|
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.
|
|
58834
59206
|
|
|
58835
59207
|
If you need explicit configuration (for a local script or a test) you can
|
|
58836
59208
|
pass an \`s3\` block to \`createClient()\` instead of relying on the injected env:
|
|
@@ -58844,7 +59216,8 @@ const sb = createClient({
|
|
|
58844
59216
|
secretAccessKey: '…',
|
|
58845
59217
|
endpoint: 'http://localhost:9000',
|
|
58846
59218
|
bucket: 'my-bucket',
|
|
58847
|
-
// region defaults to '
|
|
59219
|
+
// region defaults to 'us-east-1'
|
|
59220
|
+
// forcePathStyle defaults to true; set false for AWS S3
|
|
58848
59221
|
},
|
|
58849
59222
|
agentId: 'local-agent',
|
|
58850
59223
|
});
|
|
@@ -58852,9 +59225,9 @@ const bucket = sb.storage.from('uploads');
|
|
|
58852
59225
|
\`\`\`
|
|
58853
59226
|
|
|
58854
59227
|
Missing any required value surfaces \`s3_credentials_missing\`,
|
|
58855
|
-
\`s3_bucket_missing\` or \`agent_id_missing\` with a hint naming what to set
|
|
58856
|
-
\`region\`
|
|
58857
|
-
|
|
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\`).
|
|
58858
59231
|
|
|
58859
59232
|
## Upload, download, list, remove
|
|
58860
59233
|
|
|
@@ -60134,7 +60507,10 @@ export const configSchema = z.object({
|
|
|
60134
60507
|
|
|
60135
60508
|
That one file drives three things at once: the form the operator sees in
|
|
60136
60509
|
the dashboard, the validation applied when they save, and your types.
|
|
60137
|
-
|
|
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:
|
|
60138
60514
|
|
|
60139
60515
|
\`\`\`sh
|
|
60140
60516
|
stackbone config types
|
|
@@ -60621,22 +60997,23 @@ workspace works zero-config locally. Each variable falls back to a typed
|
|
|
60621
60997
|
config field if you pass one to \`createClient\`, but you rarely need to:
|
|
60622
60998
|
the runtime provides them all:
|
|
60623
60999
|
|
|
60624
|
-
| Env var
|
|
60625
|
-
|
|
|
60626
|
-
| \`STACKBONE_API_URL\`
|
|
60627
|
-
| \`STACKBONE_INSTALLATION_ID\`
|
|
60628
|
-
| \`STACKBONE_AGENT_ID\`
|
|
60629
|
-
| \`HMAC_SECRET\`
|
|
60630
|
-
| \`STACKBONE_POSTGRES_URL\`
|
|
60631
|
-
| \`STACKBONE_SECRET_KEY\`
|
|
60632
|
-
| \`WORKFLOW_REDIS_URL\`
|
|
60633
|
-
| \`STACKBONE_S3_ENDPOINT\`
|
|
60634
|
-
| \`STACKBONE_S3_BUCKET\`
|
|
60635
|
-
| \`STACKBONE_S3_ACCESS_KEY\`
|
|
60636
|
-
| \`STACKBONE_S3_SECRET_KEY\`
|
|
60637
|
-
| \`STACKBONE_S3_REGION\`
|
|
60638
|
-
| \`
|
|
60639
|
-
| \`
|
|
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** |
|
|
60640
61017
|
|
|
60641
61018
|
The two model-provider values are the only ones you bring yourself, and you
|
|
60642
61019
|
have two ways to do it: export them in your shell, or save them once on the
|
|
@@ -63304,7 +63681,7 @@ var createInitCommand = (ctx) => defineCommand({
|
|
|
63304
63681
|
});
|
|
63305
63682
|
//#endregion
|
|
63306
63683
|
//#region src/services/deployments.service.ts
|
|
63307
|
-
init_src$
|
|
63684
|
+
init_src$11();
|
|
63308
63685
|
init_src$13();
|
|
63309
63686
|
var registerDeployment = async (ctx, body) => {
|
|
63310
63687
|
return parseResponse$1(deploymentResponseSchema, await ctx.http.post("/api/v1/deployments", body), "POST /api/v1/deployments");
|
|
@@ -63318,7 +63695,7 @@ var readIdentityVerification = async (response) => {
|
|
|
63318
63695
|
return "unknown";
|
|
63319
63696
|
}
|
|
63320
63697
|
const state = body?.identityVerification;
|
|
63321
|
-
return state === "armed" || state === "unarmed" ? state : "unknown";
|
|
63698
|
+
return state === "armed" || state === "unarmed" || state === "untaught" ? state : "unknown";
|
|
63322
63699
|
};
|
|
63323
63700
|
var verifyDeploymentSecret = async (args) => {
|
|
63324
63701
|
const fetchImpl = args.fetchImpl ?? fetch;
|
|
@@ -63361,6 +63738,151 @@ var verifyDeploymentSecret = async (args) => {
|
|
|
63361
63738
|
});
|
|
63362
63739
|
return readIdentityVerification(response);
|
|
63363
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
|
+
};
|
|
63808
|
+
//#endregion
|
|
63809
|
+
//#region src/services/handshake.service.ts
|
|
63810
|
+
init_src$13();
|
|
63811
|
+
/** Public, credential-free handshake every deployed box serves. */
|
|
63812
|
+
var CONTRACT_PATH = "/api/contract";
|
|
63813
|
+
/**
|
|
63814
|
+
* Something is at that address, but it is not a Stackbone box: a gateway's error
|
|
63815
|
+
* page, a company site, a load balancer in front of the wrong service. Kept
|
|
63816
|
+
* distinct from "nothing answered" because the remedy is different — one is an
|
|
63817
|
+
* address to correct, the other a machine to go and look at.
|
|
63818
|
+
*/
|
|
63819
|
+
var notAHandshake = (url, status, cause) => new StackboneCliError({
|
|
63820
|
+
code: "generic",
|
|
63821
|
+
message: `${url} answered with HTTP ${status}, but not with a Stackbone box handshake`,
|
|
63822
|
+
suggestion: "Check --url points at the box itself: a company site, or a load balancer in front of the wrong service, answers just like this.",
|
|
63823
|
+
cause
|
|
63824
|
+
});
|
|
63825
|
+
/**
|
|
63826
|
+
* Asks a box which protocol version it speaks, before it is registered.
|
|
63827
|
+
*
|
|
63828
|
+
* Uses a DIRECT `fetch` — `ctx.http` refuses absolute URLs on purpose (it only
|
|
63829
|
+
* ever talks to the control plane), and this is a cross-host call to the
|
|
63830
|
+
* customer's own box. The handshake is public and takes no credential, which is
|
|
63831
|
+
* what makes it usable here: at this point in `link` nothing has been registered,
|
|
63832
|
+
* so there is no identity to present anyway.
|
|
63833
|
+
*/
|
|
63834
|
+
var readBoxContract = async (args) => {
|
|
63835
|
+
const fetchImpl = args.fetchImpl ?? fetch;
|
|
63836
|
+
let contractUrl;
|
|
63837
|
+
try {
|
|
63838
|
+
contractUrl = new URL(CONTRACT_PATH, args.url).toString();
|
|
63839
|
+
} catch (cause) {
|
|
63840
|
+
throw new StackboneCliError({
|
|
63841
|
+
code: "generic",
|
|
63842
|
+
message: `--url is not a valid URL: ${args.url}`,
|
|
63843
|
+
suggestion: "Pass the full public URL of the deployed agent, e.g. https://agent.example.com",
|
|
63844
|
+
cause
|
|
63845
|
+
});
|
|
63846
|
+
}
|
|
63847
|
+
let response;
|
|
63848
|
+
try {
|
|
63849
|
+
response = await fetchImpl(contractUrl, { method: "GET" });
|
|
63850
|
+
} catch (cause) {
|
|
63851
|
+
throw new StackboneCliError({
|
|
63852
|
+
code: "generic",
|
|
63853
|
+
message: `Nothing answered at ${args.url}, so there is no way to read which protocol version that box speaks`,
|
|
63854
|
+
suggestion: "Check --url is the public address of the running container and that this machine can reach it.",
|
|
63855
|
+
cause
|
|
63856
|
+
});
|
|
63857
|
+
}
|
|
63858
|
+
let body;
|
|
63859
|
+
try {
|
|
63860
|
+
body = await response.json();
|
|
63861
|
+
} catch (cause) {
|
|
63862
|
+
throw notAHandshake(args.url, response.status, cause);
|
|
63863
|
+
}
|
|
63864
|
+
const parsed = contractResponseSchema.safeParse(body);
|
|
63865
|
+
if (!response.ok || !parsed.success) throw notAHandshake(args.url, response.status, parsed.success ? body : parsed.error);
|
|
63866
|
+
return parsed.data;
|
|
63867
|
+
};
|
|
63868
|
+
/**
|
|
63869
|
+
* Refuses a box this product cannot register, whatever door it came through.
|
|
63870
|
+
*
|
|
63871
|
+
* Pure and total: it reads the version the handshake reported and nothing else.
|
|
63872
|
+
* The floor is `MIN_SUPPORTED_DEPLOYMENT_REGISTRATION_CONTRACT_VERSION`, NOT the
|
|
63873
|
+
* SDK-wide `MIN_SUPPORTED_CONTRACT_VERSION` — that one is 1, the handshake schema
|
|
63874
|
+
* already refuses anything below 1, and a guard built on it could never fire.
|
|
63875
|
+
* The sentence comes from the shared source the browser wizard prints too, so an
|
|
63876
|
+
* operator who tries the other door reads the same story about the same box.
|
|
63877
|
+
*/
|
|
63878
|
+
var assertBoxCanBeRegistered = (contract) => {
|
|
63879
|
+
if (supportsDeploymentRegistration(contract.version)) return;
|
|
63880
|
+
throw new StackboneCliError({
|
|
63881
|
+
code: "generic",
|
|
63882
|
+
message: deploymentRegistrationRefusal(contract.version),
|
|
63883
|
+
suggestion: "Re-run `stackbone link` once the box is running a current image."
|
|
63884
|
+
});
|
|
63885
|
+
};
|
|
63364
63886
|
//#endregion
|
|
63365
63887
|
//#region src/commands/link/link.command.ts
|
|
63366
63888
|
var requireLinkField = (value, flag, description) => {
|
|
@@ -63390,6 +63912,7 @@ var requireLinkField = (value, flag, description) => {
|
|
|
63390
63912
|
*/
|
|
63391
63913
|
var identityVerificationWarning = (state) => {
|
|
63392
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.";
|
|
63393
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.";
|
|
63394
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.";
|
|
63395
63918
|
};
|
|
@@ -63448,23 +63971,34 @@ var runLink = async (ctx, args) => {
|
|
|
63448
63971
|
const activeOrg = await getActiveOrganization(ctx);
|
|
63449
63972
|
const agent = await getAgentByOwnerAndSlug(ctx, activeOrg.slug, chosenSlug);
|
|
63450
63973
|
if (!ctx.flags.json) log.step(`Linking ${targetDir} to ${agent.slug}`);
|
|
63974
|
+
if (!ctx.flags.json) log.step("Reading the box protocol version");
|
|
63975
|
+
assertBoxCanBeRegistered(await readBoxContract({ url }));
|
|
63451
63976
|
if (!ctx.flags.json) log.step("Verifying the --secret against the deployed container");
|
|
63452
|
-
const
|
|
63977
|
+
const challengeVerification = await verifyDeploymentSecret({
|
|
63453
63978
|
url,
|
|
63454
63979
|
secret
|
|
63455
63980
|
});
|
|
63456
|
-
const
|
|
63457
|
-
if (identityWarning && !ctx.flags.json) log.warn(identityWarning);
|
|
63458
|
-
const [localDev, deployment] = await Promise.all([upsertLocalDevInstallation(ctx, {
|
|
63981
|
+
const localDev = await upsertLocalDevInstallation(ctx, {
|
|
63459
63982
|
creatorOrganizationSlug: activeOrg.slug,
|
|
63460
63983
|
agentTemplateSlug: agent.slug
|
|
63461
|
-
})
|
|
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, {
|
|
63462
63996
|
organizationSlug: activeOrg.slug,
|
|
63463
63997
|
agentSlug: agent.slug,
|
|
63464
63998
|
url,
|
|
63465
63999
|
hmacSecret: secret,
|
|
63466
64000
|
imageTag: tag
|
|
63467
|
-
})
|
|
64001
|
+
});
|
|
63468
64002
|
await writeProjectConfig(ctx, agent, {
|
|
63469
64003
|
root: targetDir,
|
|
63470
64004
|
force: true
|
|
@@ -65746,7 +66280,7 @@ if (shouldReexecUnderWinpty({
|
|
|
65746
66280
|
if (isVersionFlag(rawArgs)) {
|
|
65747
66281
|
process.stdout.write(`${formatVersionOutput({
|
|
65748
66282
|
cliVersion: STACKBONE_CLI_BUILD_VERSION,
|
|
65749
|
-
contractVersion:
|
|
66283
|
+
contractVersion: 15
|
|
65750
66284
|
})}\n`);
|
|
65751
66285
|
process.exit(0);
|
|
65752
66286
|
}
|