@stackbone/cli 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/main.js +122 -13
- package/package.json +1 -1
- package/runtime-deps/@stackbone/workflow-world/index.js +70 -70
- package/stackbone-cli-0.3.2.tgz +0 -0
- package/stackbone-cli-0.3.1.tgz +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.2] - 2026-08-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`stackbone link` refuses a box the control plane could not work with, before
|
|
15
|
+
it writes anything.** The command registered whatever answered the URL, so a
|
|
16
|
+
container too old to be registered from the browser could still be linked from
|
|
17
|
+
the terminal without a word — and the failure surfaced later, on the first
|
|
18
|
+
screen that needed something the box does not speak, with a deployment row on
|
|
19
|
+
file to explain it. `link` now reads the box's public handshake
|
|
20
|
+
(`GET <url>/api/contract`, no credential) right after it resolves the agent and
|
|
21
|
+
before it verifies `--secret`, and refuses with the same sentence the browser
|
|
22
|
+
wizard prints. A box that never answers, one that answers with something that
|
|
23
|
+
is not a handshake, and one that is simply too old each fail in their own
|
|
24
|
+
words. Nothing is written on refusal: no deployment, no local project file.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **The agent protocol the box reports goes from 14 to 15, `stackbone dev`
|
|
29
|
+
included.** The box now permits the `stackbone-signature` request header on a
|
|
30
|
+
cross-origin preflight, which is what lets a browser sign a challenge against
|
|
31
|
+
it. Nothing on the wire changes shape and no capability moves — the number is
|
|
32
|
+
the change, and it is what a caller reads to know the box has it. The origin
|
|
33
|
+
allowlist is untouched: one more permitted header is not one more permitted
|
|
34
|
+
origin.
|
|
35
|
+
- **`stackbone package` says how a managed Redis URL is written.** The generated
|
|
36
|
+
`.env` and README carried `redis://host:6379`, which no managed provider
|
|
37
|
+
accepts: they all hand out a password, some require TLS, and a password
|
|
38
|
+
containing `/`, `+`, `=` or `@` has to be percent-encoded or the URL does not
|
|
39
|
+
parse. All three are now spelled out where the operator reads them, next to the
|
|
40
|
+
note that the agent PINGs Redis before starting and stops with the reason if it
|
|
41
|
+
cannot.
|
|
42
|
+
|
|
10
43
|
## [0.3.1] - 2026-08-06
|
|
11
44
|
|
|
12
45
|
### Added
|
package/main.js
CHANGED
|
@@ -501,6 +501,25 @@ var init_approvals = __esmMin((() => {
|
|
|
501
501
|
//#endregion
|
|
502
502
|
//#region ../../libs/shared/validators/src/lib/agents/contract.ts
|
|
503
503
|
/**
|
|
504
|
+
* Whether a box speaking protocol `version` can be registered as a deployment.
|
|
505
|
+
* Pure and total: the caller passes the number the handshake reported.
|
|
506
|
+
*/
|
|
507
|
+
function supportsDeploymentRegistration(version) {
|
|
508
|
+
return version >= 15;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* The one sentence both doors print when a box is below the floor.
|
|
512
|
+
*
|
|
513
|
+
* Shared, not copied, because the wizard and `stackbone link` refuse by the same
|
|
514
|
+
* rule and an operator who tries the other door must not read a different story.
|
|
515
|
+
* For the same reason it names no door: there is no "register it from the
|
|
516
|
+
* terminal instead" any more, and the only way out it can honestly offer is the
|
|
517
|
+
* one that works everywhere — run a current image.
|
|
518
|
+
*/
|
|
519
|
+
function deploymentRegistrationRefusal(version) {
|
|
520
|
+
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.`;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
504
523
|
* Validates that the negotiated `contract.version` is at or above the
|
|
505
524
|
* version the SDK still executes against. Pure, table-driven, never throws.
|
|
506
525
|
*
|
|
@@ -604,7 +623,7 @@ var init_deployments = __esmMin((() => {
|
|
|
604
623
|
agentSlug: agentSlugSchema,
|
|
605
624
|
url: z.url(),
|
|
606
625
|
hmacSecret: z.string().min(1),
|
|
607
|
-
imageTag: z.string().min(1).max(200)
|
|
626
|
+
imageTag: z.string().min(1).max(200).optional()
|
|
608
627
|
});
|
|
609
628
|
z.object({ url: z.url() });
|
|
610
629
|
deploymentResponseSchema = z.object({
|
|
@@ -3461,7 +3480,7 @@ function createContractStore(resolved = resolveConfig({})) {
|
|
|
3461
3480
|
};
|
|
3462
3481
|
}
|
|
3463
3482
|
async function performHandshake(baseUrl, fetchImpl) {
|
|
3464
|
-
const url = `${baseUrl}${CONTRACT_PATH}`;
|
|
3483
|
+
const url = `${baseUrl}${CONTRACT_PATH$1}`;
|
|
3465
3484
|
let response;
|
|
3466
3485
|
try {
|
|
3467
3486
|
response = await fetchImpl(url, { method: "GET" });
|
|
@@ -3547,12 +3566,12 @@ function maybeLogDebug(debugLogged, key, contract) {
|
|
|
3547
3566
|
debugLogged.add(key);
|
|
3548
3567
|
console.warn(`[stackbone] contract handshake ok: baseUrl=${key} version=${contract.version} capabilities=${contract.capabilities.length}`);
|
|
3549
3568
|
}
|
|
3550
|
-
var CONTRACT_PATH;
|
|
3569
|
+
var CONTRACT_PATH$1;
|
|
3551
3570
|
var init_contract_handshake = __esmMin((() => {
|
|
3552
3571
|
init_src$13();
|
|
3553
3572
|
init_config$1();
|
|
3554
3573
|
init_result$1();
|
|
3555
|
-
CONTRACT_PATH = "/api/contract";
|
|
3574
|
+
CONTRACT_PATH$1 = "/api/contract";
|
|
3556
3575
|
}));
|
|
3557
3576
|
//#endregion
|
|
3558
3577
|
//#region ../../libs/sdk/src/contract/capability-registry.ts
|
|
@@ -10576,7 +10595,7 @@ var createBoxAuthGate = (options) => {
|
|
|
10576
10595
|
//#endregion
|
|
10577
10596
|
//#region ../../libs/runtime/workspace-runtime/src/lib/server/contract.ts
|
|
10578
10597
|
init_src$13();
|
|
10579
|
-
var STACKBONE_CLI_BUILD_VERSION = "0.3.
|
|
10598
|
+
var STACKBONE_CLI_BUILD_VERSION = "0.3.2";
|
|
10580
10599
|
/**
|
|
10581
10600
|
* Capabilities the local emulator advertises on `GET /api/contract`, derived
|
|
10582
10601
|
* from the route registry above so the advertised set and the mounted routes
|
|
@@ -10597,7 +10616,7 @@ var EMULATOR_CAPABILITIES = Object.keys({
|
|
|
10597
10616
|
"browser.provider": "/api/browser-provider"
|
|
10598
10617
|
});
|
|
10599
10618
|
var buildContractResponse = () => contractResponseSchema.parse({
|
|
10600
|
-
version:
|
|
10619
|
+
version: 15,
|
|
10601
10620
|
minSupported: 1,
|
|
10602
10621
|
capabilities: [...EMULATOR_CAPABILITIES],
|
|
10603
10622
|
build: {
|
|
@@ -40738,8 +40757,7 @@ var redisConnection = (redisUrl) => {
|
|
|
40738
40757
|
const db = path === "" ? void 0 : Number(path);
|
|
40739
40758
|
if (db !== void 0 && !Number.isInteger(db)) throw new Error(`Invalid Redis database in URL path: '${url.pathname}' (expected an integer)`);
|
|
40740
40759
|
return {
|
|
40741
|
-
|
|
40742
|
-
port: url.port ? Number(url.port) : 6379,
|
|
40760
|
+
url: redisUrl,
|
|
40743
40761
|
...db === void 0 ? {} : { db },
|
|
40744
40762
|
maxRetriesPerRequest: null
|
|
40745
40763
|
};
|
|
@@ -41160,7 +41178,7 @@ var init_connections = __esmMin((() => {
|
|
|
41160
41178
|
buildAgentEnv = (base, injection, options) => {
|
|
41161
41179
|
const env = {
|
|
41162
41180
|
...base,
|
|
41163
|
-
[STACKBONE_ENV_KEYS.contractVersion]: String(
|
|
41181
|
+
[STACKBONE_ENV_KEYS.contractVersion]: String(15),
|
|
41164
41182
|
[STACKBONE_ENV_KEYS.agentId]: injection.agentId,
|
|
41165
41183
|
[STACKBONE_ENV_KEYS.postgresUrl]: injection.services.postgresUrl,
|
|
41166
41184
|
[STACKBONE_ENV_KEYS.s3Endpoint]: injection.services.s3Endpoint,
|
|
@@ -45043,7 +45061,8 @@ var buildEmulatorApp = (opts, hooks = {}) => {
|
|
|
45043
45061
|
"x-stackbone-session",
|
|
45044
45062
|
"x-stackbone-conversation",
|
|
45045
45063
|
"x-api-key",
|
|
45046
|
-
"anthropic-version"
|
|
45064
|
+
"anthropic-version",
|
|
45065
|
+
STACKBONE_SIGNATURE_HEADER
|
|
45047
45066
|
],
|
|
45048
45067
|
exposeHeaders: ["X-Stackbone-Run-Id"]
|
|
45049
45068
|
}));
|
|
@@ -49159,7 +49178,9 @@ STACKBONE_SECRET_KEY=${secretKey}
|
|
|
49159
49178
|
# agent creates its own schema on first boot. It must have the pgvector extension
|
|
49160
49179
|
# available — see the README.
|
|
49161
49180
|
# DATABASE_URL=postgres://user:password@host:5432/dbname?sslmode=require
|
|
49162
|
-
# WORKFLOW_REDIS_URL=redis://host:6379
|
|
49181
|
+
# WORKFLOW_REDIS_URL=redis://default:password@host:6379
|
|
49182
|
+
# Managed Redis always wants a password, and some require TLS — use rediss:// for
|
|
49183
|
+
# those. Percent-encode a password containing / + = or @, or the URL will not parse.
|
|
49163
49184
|
|
|
49164
49185
|
# ---- Optional ----------------------------------------------------------------
|
|
49165
49186
|
# Set if your database role is not allowed to create schemas; you then apply the
|
|
@@ -49244,6 +49265,14 @@ and apply these by hand first, in filename order:
|
|
|
49244
49265
|
|
|
49245
49266
|
Both live inside the image; \`docker run --rm ${input.imageTag} ls <path>\` lists them.
|
|
49246
49267
|
|
|
49268
|
+
Redis needs no preparation, only a reachable URL. Managed instances hand out a
|
|
49269
|
+
password, so the URL carries one — \`redis://default:password@host:6379\`. Use
|
|
49270
|
+
\`rediss://\` where the provider requires TLS (Upstash, ElastiCache with encryption
|
|
49271
|
+
in transit), and percent-encode a password containing \`/\`, \`+\`, \`=\` or \`@\`. The
|
|
49272
|
+
agent PINGs Redis before it starts and stops with the reason if it cannot: a
|
|
49273
|
+
rejected password fails right away, and a Redis still starting up is retried for
|
|
49274
|
+
30 seconds first.
|
|
49275
|
+
|
|
49247
49276
|
## Running it somewhere other than Compose
|
|
49248
49277
|
|
|
49249
49278
|
The agent serves on port **${RUNTIME_PORT}** by default, and binds whatever
|
|
@@ -54060,7 +54089,7 @@ var buildHandler$2 = (ctx, flags) => protectedCommand(async (innerCtx) => {
|
|
|
54060
54089
|
tunnelUrl: session.publicUrl,
|
|
54061
54090
|
localUrl: session.emulatorUrl,
|
|
54062
54091
|
deeplink,
|
|
54063
|
-
contractVersion:
|
|
54092
|
+
contractVersion: 15,
|
|
54064
54093
|
capabilityCount: EMULATOR_CAPABILITIES.length,
|
|
54065
54094
|
agentName: agent.name,
|
|
54066
54095
|
colors: process.stdout.isTTY === true
|
|
@@ -63362,6 +63391,84 @@ var verifyDeploymentSecret = async (args) => {
|
|
|
63362
63391
|
return readIdentityVerification(response);
|
|
63363
63392
|
};
|
|
63364
63393
|
//#endregion
|
|
63394
|
+
//#region src/services/handshake.service.ts
|
|
63395
|
+
init_src$13();
|
|
63396
|
+
/** Public, credential-free handshake every deployed box serves. */
|
|
63397
|
+
var CONTRACT_PATH = "/api/contract";
|
|
63398
|
+
/**
|
|
63399
|
+
* Something is at that address, but it is not a Stackbone box: a gateway's error
|
|
63400
|
+
* page, a company site, a load balancer in front of the wrong service. Kept
|
|
63401
|
+
* distinct from "nothing answered" because the remedy is different — one is an
|
|
63402
|
+
* address to correct, the other a machine to go and look at.
|
|
63403
|
+
*/
|
|
63404
|
+
var notAHandshake = (url, status, cause) => new StackboneCliError({
|
|
63405
|
+
code: "generic",
|
|
63406
|
+
message: `${url} answered with HTTP ${status}, but not with a Stackbone box handshake`,
|
|
63407
|
+
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.",
|
|
63408
|
+
cause
|
|
63409
|
+
});
|
|
63410
|
+
/**
|
|
63411
|
+
* Asks a box which protocol version it speaks, before it is registered.
|
|
63412
|
+
*
|
|
63413
|
+
* Uses a DIRECT `fetch` — `ctx.http` refuses absolute URLs on purpose (it only
|
|
63414
|
+
* ever talks to the control plane), and this is a cross-host call to the
|
|
63415
|
+
* customer's own box. The handshake is public and takes no credential, which is
|
|
63416
|
+
* what makes it usable here: at this point in `link` nothing has been registered,
|
|
63417
|
+
* so there is no identity to present anyway.
|
|
63418
|
+
*/
|
|
63419
|
+
var readBoxContract = async (args) => {
|
|
63420
|
+
const fetchImpl = args.fetchImpl ?? fetch;
|
|
63421
|
+
let contractUrl;
|
|
63422
|
+
try {
|
|
63423
|
+
contractUrl = new URL(CONTRACT_PATH, args.url).toString();
|
|
63424
|
+
} catch (cause) {
|
|
63425
|
+
throw new StackboneCliError({
|
|
63426
|
+
code: "generic",
|
|
63427
|
+
message: `--url is not a valid URL: ${args.url}`,
|
|
63428
|
+
suggestion: "Pass the full public URL of the deployed agent, e.g. https://agent.example.com",
|
|
63429
|
+
cause
|
|
63430
|
+
});
|
|
63431
|
+
}
|
|
63432
|
+
let response;
|
|
63433
|
+
try {
|
|
63434
|
+
response = await fetchImpl(contractUrl, { method: "GET" });
|
|
63435
|
+
} catch (cause) {
|
|
63436
|
+
throw new StackboneCliError({
|
|
63437
|
+
code: "generic",
|
|
63438
|
+
message: `Nothing answered at ${args.url}, so there is no way to read which protocol version that box speaks`,
|
|
63439
|
+
suggestion: "Check --url is the public address of the running container and that this machine can reach it.",
|
|
63440
|
+
cause
|
|
63441
|
+
});
|
|
63442
|
+
}
|
|
63443
|
+
let body;
|
|
63444
|
+
try {
|
|
63445
|
+
body = await response.json();
|
|
63446
|
+
} catch (cause) {
|
|
63447
|
+
throw notAHandshake(args.url, response.status, cause);
|
|
63448
|
+
}
|
|
63449
|
+
const parsed = contractResponseSchema.safeParse(body);
|
|
63450
|
+
if (!response.ok || !parsed.success) throw notAHandshake(args.url, response.status, parsed.success ? body : parsed.error);
|
|
63451
|
+
return parsed.data;
|
|
63452
|
+
};
|
|
63453
|
+
/**
|
|
63454
|
+
* Refuses a box this product cannot register, whatever door it came through.
|
|
63455
|
+
*
|
|
63456
|
+
* Pure and total: it reads the version the handshake reported and nothing else.
|
|
63457
|
+
* The floor is `MIN_SUPPORTED_DEPLOYMENT_REGISTRATION_CONTRACT_VERSION`, NOT the
|
|
63458
|
+
* SDK-wide `MIN_SUPPORTED_CONTRACT_VERSION` — that one is 1, the handshake schema
|
|
63459
|
+
* already refuses anything below 1, and a guard built on it could never fire.
|
|
63460
|
+
* The sentence comes from the shared source the browser wizard prints too, so an
|
|
63461
|
+
* operator who tries the other door reads the same story about the same box.
|
|
63462
|
+
*/
|
|
63463
|
+
var assertBoxCanBeRegistered = (contract) => {
|
|
63464
|
+
if (supportsDeploymentRegistration(contract.version)) return;
|
|
63465
|
+
throw new StackboneCliError({
|
|
63466
|
+
code: "generic",
|
|
63467
|
+
message: deploymentRegistrationRefusal(contract.version),
|
|
63468
|
+
suggestion: "Re-run `stackbone link` once the box is running a current image."
|
|
63469
|
+
});
|
|
63470
|
+
};
|
|
63471
|
+
//#endregion
|
|
63365
63472
|
//#region src/commands/link/link.command.ts
|
|
63366
63473
|
var requireLinkField = (value, flag, description) => {
|
|
63367
63474
|
const trimmed = (value ?? "").trim();
|
|
@@ -63448,6 +63555,8 @@ var runLink = async (ctx, args) => {
|
|
|
63448
63555
|
const activeOrg = await getActiveOrganization(ctx);
|
|
63449
63556
|
const agent = await getAgentByOwnerAndSlug(ctx, activeOrg.slug, chosenSlug);
|
|
63450
63557
|
if (!ctx.flags.json) log.step(`Linking ${targetDir} to ${agent.slug}`);
|
|
63558
|
+
if (!ctx.flags.json) log.step("Reading the box protocol version");
|
|
63559
|
+
assertBoxCanBeRegistered(await readBoxContract({ url }));
|
|
63451
63560
|
if (!ctx.flags.json) log.step("Verifying the --secret against the deployed container");
|
|
63452
63561
|
const identityVerification = await verifyDeploymentSecret({
|
|
63453
63562
|
url,
|
|
@@ -65746,7 +65855,7 @@ if (shouldReexecUnderWinpty({
|
|
|
65746
65855
|
if (isVersionFlag(rawArgs)) {
|
|
65747
65856
|
process.stdout.write(`${formatVersionOutput({
|
|
65748
65857
|
cliVersion: STACKBONE_CLI_BUILD_VERSION,
|
|
65749
|
-
contractVersion:
|
|
65858
|
+
contractVersion: 15
|
|
65750
65859
|
})}\n`);
|
|
65751
65860
|
process.exit(0);
|
|
65752
65861
|
}
|