@bnbagent/studio-cli 0.0.11-alpha.2 → 0.0.11-alpha.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/dist/bag.js +1122 -952
- package/dist/{chunk-TTPOH453.js → chunk-MIYN6U2D.js} +79 -31
- package/dist/{deployCli-K55GXDVO.js → deployCli-AED2MLUB.js} +1 -1
- package/package.json +2 -2
- package/skills/references/bnbagent-studio-selling-via-b402.md +6 -4
- package/skills/references/bnbagent-studio-use-azure-foundry.md +34 -7
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
} from "./chunk-RO726HJG.js";
|
|
9
9
|
|
|
10
10
|
// src/cli/_deploy/deployCli.ts
|
|
11
|
-
import * as
|
|
11
|
+
import * as fs5 from "fs";
|
|
12
12
|
import * as os from "os";
|
|
13
|
-
import * as
|
|
13
|
+
import * as path6 from "path";
|
|
14
14
|
import {
|
|
15
15
|
findSubProjectRoot as findSubProjectRoot2,
|
|
16
16
|
loadStudioToml as loadStudioToml3
|
|
@@ -662,36 +662,82 @@ function readAwsTarget(root, targetName = DEFAULT_TARGET_NAME) {
|
|
|
662
662
|
}
|
|
663
663
|
|
|
664
664
|
// src/cli/_deploy/x402.ts
|
|
665
|
-
import * as
|
|
665
|
+
import * as path5 from "path";
|
|
666
666
|
import {
|
|
667
667
|
envLocalPath,
|
|
668
668
|
findSubProjectRoot,
|
|
669
669
|
loadStudioToml as loadStudioToml2
|
|
670
670
|
} from "@bnbagent/studio-runtime/config";
|
|
671
671
|
|
|
672
|
-
// src/cli/
|
|
672
|
+
// src/cli/_packageMetadata.ts
|
|
673
673
|
import * as fs3 from "fs";
|
|
674
674
|
import * as path3 from "path";
|
|
675
|
+
import { fileURLToPath } from "url";
|
|
676
|
+
function packageRoot() {
|
|
677
|
+
let dir = path3.dirname(fileURLToPath(import.meta.url));
|
|
678
|
+
for (; ; ) {
|
|
679
|
+
const packageJson = path3.join(dir, "package.json");
|
|
680
|
+
if (fs3.existsSync(packageJson)) return dir;
|
|
681
|
+
const parent = path3.dirname(dir);
|
|
682
|
+
if (parent === dir) {
|
|
683
|
+
throw new Error("cannot locate the studio-cli package root");
|
|
684
|
+
}
|
|
685
|
+
dir = parent;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function studioCliVersion() {
|
|
689
|
+
if ("0.0.11-alpha.3") {
|
|
690
|
+
return "0.0.11-alpha.3";
|
|
691
|
+
}
|
|
692
|
+
const file = path3.join(packageRoot(), "package.json");
|
|
693
|
+
const pkg = JSON.parse(fs3.readFileSync(file, "utf-8"));
|
|
694
|
+
const value = String(pkg.version ?? "");
|
|
695
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(value)) {
|
|
696
|
+
throw new Error(
|
|
697
|
+
`studio-cli package.json must contain a valid version in ${file}`
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
return value;
|
|
701
|
+
}
|
|
702
|
+
function pnpmVersion() {
|
|
703
|
+
if ("10.24.0") {
|
|
704
|
+
return "10.24.0";
|
|
705
|
+
}
|
|
706
|
+
const file = path3.join(packageRoot(), "package.json");
|
|
707
|
+
const pkg = JSON.parse(fs3.readFileSync(file, "utf-8"));
|
|
708
|
+
const value = String(pkg.packageManager ?? "");
|
|
709
|
+
const match = /^pnpm@(.+)$/u.exec(value);
|
|
710
|
+
if (!match?.[1]) {
|
|
711
|
+
throw new Error(
|
|
712
|
+
`studio-cli packageManager must pin pnpm (expected "pnpm@<version>" in ${file})`
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
return match[1];
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// src/cli/utils/envFile.ts
|
|
719
|
+
import * as fs4 from "fs";
|
|
720
|
+
import * as path4 from "path";
|
|
675
721
|
var KEY_LINE_RE = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/;
|
|
676
722
|
function isValidKey(key) {
|
|
677
723
|
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);
|
|
678
724
|
}
|
|
679
725
|
function writePrivate(filePath, text) {
|
|
680
|
-
|
|
681
|
-
|
|
726
|
+
fs4.writeFileSync(filePath, text, { encoding: "utf-8", mode: 384 });
|
|
727
|
+
fs4.chmodSync(filePath, 384);
|
|
682
728
|
}
|
|
683
729
|
function setEnvVar(filePath, key, value) {
|
|
684
730
|
if (!isValidKey(key)) {
|
|
685
731
|
throw new Error(`invalid env var name: '${key}'`);
|
|
686
732
|
}
|
|
687
733
|
const newLine = `${key}=${value}`;
|
|
688
|
-
|
|
689
|
-
if (!
|
|
734
|
+
fs4.mkdirSync(path4.dirname(filePath), { recursive: true });
|
|
735
|
+
if (!fs4.existsSync(filePath)) {
|
|
690
736
|
writePrivate(filePath, `${newLine}
|
|
691
737
|
`);
|
|
692
738
|
return true;
|
|
693
739
|
}
|
|
694
|
-
const original =
|
|
740
|
+
const original = fs4.readFileSync(filePath, "utf-8");
|
|
695
741
|
const lines = original.split(/\r?\n/);
|
|
696
742
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
697
743
|
lines.pop();
|
|
@@ -722,10 +768,10 @@ function setEnvVar(filePath, key, value) {
|
|
|
722
768
|
return true;
|
|
723
769
|
}
|
|
724
770
|
function getEnvVar(filePath, key) {
|
|
725
|
-
if (!
|
|
771
|
+
if (!fs4.existsSync(filePath)) {
|
|
726
772
|
return null;
|
|
727
773
|
}
|
|
728
|
-
for (const line of
|
|
774
|
+
for (const line of fs4.readFileSync(filePath, "utf-8").split(/\r?\n/)) {
|
|
729
775
|
const m = KEY_LINE_RE.exec(line);
|
|
730
776
|
if (m && m[1] === key) {
|
|
731
777
|
const eq = line.indexOf("=");
|
|
@@ -779,7 +825,7 @@ function loadDeployConfig(root) {
|
|
|
779
825
|
try {
|
|
780
826
|
return {
|
|
781
827
|
agentRoot,
|
|
782
|
-
cfg: loadStudioToml2(
|
|
828
|
+
cfg: loadStudioToml2(path5.join(agentRoot, "studio.toml"))
|
|
783
829
|
};
|
|
784
830
|
} catch {
|
|
785
831
|
return { agentRoot, cfg: {} };
|
|
@@ -799,7 +845,7 @@ function x402DeploySummary(root, destination, publicUrl) {
|
|
|
799
845
|
const azure = runtime === "azure-foundry";
|
|
800
846
|
const runtimeLabel = azure ? "Azure Foundry" : "AgentCore";
|
|
801
847
|
const tunnelLine = azure ? 'The front wraps each request as envelope-v1 JSON, {"v":1,"method":"POST","path":"/x402","headers":{...},"body":"<base64>"}, inside an Entra-authenticated Foundry invocation (POST \u2026/agents/<name>/endpoint/protocols/invocations with an https://ai.azure.com bearer). Limits: 1 MiB request, 5 MiB response, no streaming.' : 'The front wraps each request as envelope-v1 JSON, {"v":1,"method":"POST","path":"/x402","headers":{...},"body":"<base64>"}, inside an authenticated AgentCore invocation. The default Bag self-deploy uses Cognito OAuth over HTTPS; IAM runtimes may use SDK/SigV4. Limits: 1 MiB request, 5 MiB response, no streaming.';
|
|
802
|
-
const gatewayDocLine = azure ?
|
|
848
|
+
const gatewayDocLine = azure ? `Gateway example: https://unpkg.com/@bnbagent/studio-cli@${studioCliVersion()}/skills/references/bnbagent-studio-use-azure-foundry.md#x402-external-gateway` : "Gateway example: https://github.com/bnb-chain/bnbagent-studio/blob/main/docs/guides/self-hosted-x402-gateway.md";
|
|
803
849
|
if (pricing.kind === "invalid") {
|
|
804
850
|
return "x402 rail is DORMANT: price_usd is invalid; run `bag doctor`.";
|
|
805
851
|
}
|
|
@@ -899,7 +945,7 @@ function tableOf(data, key) {
|
|
|
899
945
|
function descriptorName(root) {
|
|
900
946
|
try {
|
|
901
947
|
const raw = JSON.parse(
|
|
902
|
-
|
|
948
|
+
fs5.readFileSync(path6.join(root, "agentcore", "agentcore.json"), "utf-8")
|
|
903
949
|
);
|
|
904
950
|
if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
|
|
905
951
|
const name = raw.name;
|
|
@@ -936,9 +982,9 @@ function providerPassthrough(studio, table4) {
|
|
|
936
982
|
}
|
|
937
983
|
function buildDeploySpec(root, opts) {
|
|
938
984
|
const agentRoot = findSubProjectRoot2("agent", root) ?? root;
|
|
939
|
-
const studio = loadStudioToml3(
|
|
985
|
+
const studio = loadStudioToml3(path6.join(agentRoot, "studio.toml"));
|
|
940
986
|
const name = String(
|
|
941
|
-
opts.nameOverride || descriptorName(root) || tableOf(studio, "project").name ||
|
|
987
|
+
opts.nameOverride || descriptorName(root) || tableOf(studio, "project").name || path6.basename(root)
|
|
942
988
|
);
|
|
943
989
|
const stack = tableOf(studio, "stack");
|
|
944
990
|
const payments = tableOf(studio, "payments");
|
|
@@ -960,7 +1006,7 @@ function buildDeploySpec(root, opts) {
|
|
|
960
1006
|
OTEL_TRACES_EXPORTER: "none"
|
|
961
1007
|
};
|
|
962
1008
|
if (isContainer) {
|
|
963
|
-
deploy.dockerfile =
|
|
1009
|
+
deploy.dockerfile = path6.join(agentRoot, "Dockerfile");
|
|
964
1010
|
} else {
|
|
965
1011
|
if (!opts.zip) {
|
|
966
1012
|
throw new Error("zip packaging requires a prebuilt zip artifact");
|
|
@@ -1066,27 +1112,27 @@ function renderSecretEnvFile(payload) {
|
|
|
1066
1112
|
`).join("");
|
|
1067
1113
|
}
|
|
1068
1114
|
function writePrivateSync(filePath, content) {
|
|
1069
|
-
|
|
1070
|
-
|
|
1115
|
+
fs5.writeFileSync(filePath, content, { mode: 384 });
|
|
1116
|
+
fs5.chmodSync(filePath, 384);
|
|
1071
1117
|
}
|
|
1072
1118
|
async function withDeployFiles(root, opts, fn) {
|
|
1073
|
-
const scratch =
|
|
1119
|
+
const scratch = fs5.mkdtempSync(path6.join(os.tmpdir(), "bag-deploy-"));
|
|
1074
1120
|
try {
|
|
1075
1121
|
let zip;
|
|
1076
1122
|
if (opts.packaging === "zip") {
|
|
1077
1123
|
const agentRoot = findSubProjectRoot2("agent", root) ?? root;
|
|
1078
1124
|
const studio = loadStudioToml3(
|
|
1079
|
-
|
|
1125
|
+
path6.join(agentRoot, "studio.toml")
|
|
1080
1126
|
);
|
|
1081
1127
|
const stack = tableOf(studio, "stack");
|
|
1082
1128
|
const faces = stackFaces(stack, tableOf(studio, "payments"));
|
|
1083
|
-
const outZip =
|
|
1129
|
+
const outZip = path6.join(scratch, "code.zip");
|
|
1084
1130
|
const built = await (opts.buildZipFn ?? buildZip)(agentRoot, outZip, {
|
|
1085
1131
|
protocols: faces
|
|
1086
1132
|
});
|
|
1087
1133
|
zip = { path: outZip, entrypoint: built.entrypoint };
|
|
1088
1134
|
}
|
|
1089
|
-
const secretFile =
|
|
1135
|
+
const secretFile = path6.join(scratch, "secrets.env");
|
|
1090
1136
|
const inline = opts.inlineSecrets;
|
|
1091
1137
|
const payload = opts.secrets ?? {};
|
|
1092
1138
|
const spec = buildDeploySpec(root, {
|
|
@@ -1100,11 +1146,11 @@ async function withDeployFiles(root, opts, fn) {
|
|
|
1100
1146
|
if (!inline && Object.keys(payload).length > 0) {
|
|
1101
1147
|
writePrivateSync(secretFile, renderSecretEnvFile(payload));
|
|
1102
1148
|
}
|
|
1103
|
-
const configPath =
|
|
1149
|
+
const configPath = path6.join(scratch, "agent-deploy.toml");
|
|
1104
1150
|
writePrivateSync(configPath, tomlStringify(spec.doc));
|
|
1105
1151
|
return await fn({ configPath, scratchDir: scratch }, spec);
|
|
1106
1152
|
} finally {
|
|
1107
|
-
|
|
1153
|
+
fs5.rmSync(scratch, { recursive: true, force: true });
|
|
1108
1154
|
}
|
|
1109
1155
|
}
|
|
1110
1156
|
var BUNX_HINT = "error: could not start bnbagent-deploy (bunx not found); install Bun 1.3+ or set BNBAGENT_DEPLOY_COMMAND.";
|
|
@@ -1129,7 +1175,7 @@ function bunxInstallDir(environment = process.env) {
|
|
|
1129
1175
|
return null;
|
|
1130
1176
|
}
|
|
1131
1177
|
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
1132
|
-
return uid === null ? null :
|
|
1178
|
+
return uid === null ? null : path6.join(os.tmpdir(), `bunx-${uid}-${DEPLOY_CLI_PACKAGE}`);
|
|
1133
1179
|
}
|
|
1134
1180
|
async function bunxTreeIsIncomplete() {
|
|
1135
1181
|
const [bin, ...prefix] = deployCommand();
|
|
@@ -1148,14 +1194,14 @@ async function withBunxSelfHeal(hasFailed, attempt) {
|
|
|
1148
1194
|
if (dir === null || !await bunxTreeIsIncomplete()) {
|
|
1149
1195
|
return first;
|
|
1150
1196
|
}
|
|
1151
|
-
if (!
|
|
1197
|
+
if (!fs5.existsSync(dir)) {
|
|
1152
1198
|
printErr(BUNX_TREE_HINT);
|
|
1153
1199
|
return first;
|
|
1154
1200
|
}
|
|
1155
1201
|
printErr(
|
|
1156
1202
|
`note: ${DEPLOY_CLI_PACKAGE} failed to load its dependencies; clearing its incomplete bunx install and retrying once (${dir}).`
|
|
1157
1203
|
);
|
|
1158
|
-
|
|
1204
|
+
fs5.rmSync(dir, { recursive: true, force: true });
|
|
1159
1205
|
const second = await attempt();
|
|
1160
1206
|
if (hasFailed(second)) {
|
|
1161
1207
|
printErr(BUNX_TREE_HINT);
|
|
@@ -1228,9 +1274,9 @@ async function runPlatformAccountCommand(argv, opts = {}) {
|
|
|
1228
1274
|
if (!opts.slug) {
|
|
1229
1275
|
return run([...argv, "--provider", "bnb"]);
|
|
1230
1276
|
}
|
|
1231
|
-
const scratch =
|
|
1277
|
+
const scratch = fs5.mkdtempSync(path6.join(os.tmpdir(), "bag-platform-"));
|
|
1232
1278
|
try {
|
|
1233
|
-
const configPath =
|
|
1279
|
+
const configPath = path6.join(scratch, "agent-deploy.toml");
|
|
1234
1280
|
writePrivateSync(
|
|
1235
1281
|
configPath,
|
|
1236
1282
|
tomlStringify({
|
|
@@ -1241,7 +1287,7 @@ async function runPlatformAccountCommand(argv, opts = {}) {
|
|
|
1241
1287
|
);
|
|
1242
1288
|
return await run([...argv, "--provider", "bnb", "-f", configPath], scratch);
|
|
1243
1289
|
} finally {
|
|
1244
|
-
|
|
1290
|
+
fs5.rmSync(scratch, { recursive: true, force: true });
|
|
1245
1291
|
}
|
|
1246
1292
|
}
|
|
1247
1293
|
|
|
@@ -1281,6 +1327,8 @@ export {
|
|
|
1281
1327
|
deployChecks,
|
|
1282
1328
|
render,
|
|
1283
1329
|
readAwsTarget,
|
|
1330
|
+
studioCliVersion,
|
|
1331
|
+
pnpmVersion,
|
|
1284
1332
|
B402_RUNTIME_KEYS,
|
|
1285
1333
|
X402_CAPABLE_RUNTIMES,
|
|
1286
1334
|
commerceRails,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bnbagent/studio-cli",
|
|
3
|
-
"version": "0.0.11-alpha.
|
|
3
|
+
"version": "0.0.11-alpha.3",
|
|
4
4
|
"description": "Skills-first toolkit and bag CLI for BNB Chain seller agents: ERC-8004 identity, ERC-8183 escrowed commerce, and x402 payments.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bnb-chain",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"tar": "^7.4.0",
|
|
55
55
|
"viem": "^2.54.0",
|
|
56
56
|
"yaml": "^2.9.0",
|
|
57
|
-
"@bnbagent/studio-runtime": "0.0.11-alpha.
|
|
57
|
+
"@bnbagent/studio-runtime": "0.0.11-alpha.3"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@a2a-js/sdk": "^0.3.14",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bnbagent-studio-selling-via-b402
|
|
3
|
-
description: When the user wants a bnbagent-studio agent to sell paid or FREE HTTP requests through the B402-backed x402 rail. Owns the explicit pricing choice and, for PAID mode, per-agent merchant onboarding, RSA key preparation, egress-IP allowlisting, sandbox/production separation, B402 environment setup, seller status checks, and activation by redeploy (managed platform or self-hosted
|
|
3
|
+
description: When the user wants a bnbagent-studio agent to sell paid or FREE HTTP requests through the B402-backed x402 rail. Owns the explicit pricing choice and, for PAID mode, per-agent merchant onboarding, RSA key preparation, egress-IP allowlisting, sandbox/production separation, B402 environment setup, seller status checks, and activation by redeploy (managed platform, self-hosted AgentCore, or self-hosted Azure Foundry).
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
> **Reference file** of the `bnbagent-studio` router skill, installed at `bnbagent-studio/references/` and loaded on demand (not a standalone skill). Route here via the router's decision tree.
|
|
@@ -30,7 +30,7 @@ bag config set payments.x402_seller.price_usd 0
|
|
|
30
30
|
|
|
31
31
|
`"0"` means anonymous FREE passthrough. The runtime returns work directly and does not issue a 402 challenge, call B402 `/supported`/verify/settle, transfer U, or write an `x402_sell` settlement audit. B402 credentials are ignored and not synchronized. Run `bag x402 sell status`, `bag doctor`, and `bag deploy prepare`; all must label the route FREE.
|
|
32
32
|
|
|
33
|
-
This is unrestricted public access. Confirm that intent before continuing. Managed platform still publishes the route through its gateway; self-hosted AgentCore still
|
|
33
|
+
This is unrestricted public access. Confirm that intent before continuing. Managed platform still publishes the route through its gateway; self-hosted AgentCore and Azure Foundry deploys still need an envelope-v1 front. If FREE is the selected product, skip the merchant/RSA/IP sections below.
|
|
34
34
|
|
|
35
35
|
## Generate the agent's RSA material
|
|
36
36
|
|
|
@@ -64,7 +64,7 @@ The current B402 request-signing contract explicitly requires a 1024-bit RSA key
|
|
|
64
64
|
|
|
65
65
|
B402 allowlists the merchant's **outbound** (egress) IPs, the addresses the agent's facilitator calls come FROM. Submit every part that applies to your deployment target:
|
|
66
66
|
|
|
67
|
-
1. **Platform Relay egress IPs (managed-platform deploys)**: the addresses that the platform B402 Relay uses to reach the facilitator. The managed deployment worker points
|
|
67
|
+
1. **Platform Relay egress IPs (managed-platform deploys)**: the addresses that the platform B402 Relay uses to reach the facilitator. The managed deployment worker points the runtime copy of `B402_BASE_URL` at this Relay on both managed backends (AgentCore and Azure Foundry). Refresh and read the interactive session without printing its bearer:
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
70
|
bag platform whoami >/dev/null
|
|
@@ -89,6 +89,8 @@ B402 allowlists the merchant's **outbound** (egress) IPs, the addresses the agen
|
|
|
89
89
|
|
|
90
90
|
As an alternative, use AWS-supported AgentCore VPC mode with a private subnet, NAT Gateway, and Elastic IP. Submit the Elastic IP and keep `B402_BASE_URL` pointed at the facilitator. Studio does not deploy or manage that AWS network.
|
|
91
91
|
|
|
92
|
+
4. **Self-hosted Azure Foundry egress (self-deploys)**: Foundry hosted-agent containers have floating egress just like AgentCore, so the agent must NOT call the facilitator directly. Run the envelope gateway on a Container Apps workload-profiles environment whose subnet has a NAT Gateway with a Standard static public IP, co-host a restricted B402 forwarder there (the AWS guide's Relay example works verbatim — the NAT Gateway replaces its fixed-IP host requirement), point the runtime `B402_BASE_URL` at that forwarder, and submit the NAT Gateway IP. The environment type and VNet cannot be changed after creation; see the [Azure external-gateway reference](bnbagent-studio-use-azure-foundry.md#x402-external-gateway). Studio does not deploy or manage that Azure network.
|
|
93
|
+
|
|
92
94
|
Do not add the public inbound gateway IP, a transient build-runner IP, or guessed addresses. If the whitelist endpoint is unreachable, stop onboarding and confirm the platform environment with the operator.
|
|
93
95
|
|
|
94
96
|
## Submit the B402 merchant application
|
|
@@ -101,7 +103,7 @@ Apply through the [B402 developer account application](https://developers.binanc
|
|
|
101
103
|
| Email | Primary contact email address |
|
|
102
104
|
| Wallet address | The agent wallet for that environment |
|
|
103
105
|
| Public key | The contents of `.studio/b402/<environment>/public.der.b64` |
|
|
104
|
-
| IP allowlist | For managed deploys, every platform Relay IP from `/v1/b402/whitelist-ips`; for self-hosted
|
|
106
|
+
| IP allowlist | For managed deploys (both backends), every platform Relay IP from `/v1/b402/whitelist-ips`; for self-hosted AgentCore, the user's Relay or VPC NAT Elastic IP; for self-hosted Azure Foundry, the ACA NAT Gateway static IP; add the local public IP when `bag dev` must reach B402 directly |
|
|
105
107
|
| Webhook callback URL | Supply only when the integration uses callbacks |
|
|
106
108
|
|
|
107
109
|
Keep the two environments isolated:
|
|
@@ -7,7 +7,9 @@ description: When the user wants to deploy or operate a bnbagent-studio project
|
|
|
7
7
|
|
|
8
8
|
# bnbagent-studio-use-azure-foundry
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Azure Foundry is listed in `bag init --help` and the deploy provider menu. The
|
|
11
|
+
Foundry data plane, Entra scope, unified entrypoint, deployment flow, and x402
|
|
12
|
+
envelope tunnel were verified end to end in 2026-08.
|
|
11
13
|
|
|
12
14
|
Procedure for deploying and operating the seller Agent on **Azure AI Foundry Hosted Agents** (`[stack].runtime = "azure-foundry"`). ALL cloud execution is **delegated to the pinned `@bnbagent/deploy-cli`** (run via `bunx --bun`; override with `BNBAGENT_DEPLOY_COMMAND`), whose Azure provider is **SDK/REST-only**. Studio never shells out to (or requires) the `az` / `azd` CLIs. Direct user-owned deployment uses browser/local credentials and may self-heal RBAC; the managed platform uses environment credentials in `ambient-only` mode and only probes RBAC.
|
|
13
15
|
|
|
@@ -21,15 +23,15 @@ Procedure for deploying and operating the seller Agent on **Azure AI Foundry Hos
|
|
|
21
23
|
|
|
22
24
|
> **Deploy model: CONTAINER-ONLY.** The deploy-cli Azure provider rejects Node zip artifacts, so every azure-foundry deploy builds the scaffolded `app/agent/Dockerfile` **locally with Docker** (linux/amd64) and pushes it to the auto-provisioned Azure Container Registry; Foundry Agent Service pulls and runs the image. A running Docker daemon is required.
|
|
23
25
|
|
|
24
|
-
> **Protocol
|
|
26
|
+
> **Protocol:** A2A, X402-only, and A2A+X402 projects are supported. The deployed Node host speaks both Foundry container contracts on `:8088`: `GET /readiness`, pass-through `POST /invocations` for user-owned Azure, and OpenAI-compatible `POST /responses` for managed incoming A2A. Neither endpoint is native MCP streamable HTTP; `bag init` and provider selection reject azure-foundry + MCP instead of deploying a container that can never become ready.
|
|
25
27
|
|
|
26
|
-
> **Auth is
|
|
28
|
+
> **Auth is explicit.** User-owned Azure needs a saved delegated browser sign-in, or an ambient Azure credential chain in CI. Deployment itself never shells out to `az` or `azd`, and a non-interactive deploy cannot open a browser.
|
|
27
29
|
|
|
28
30
|
## Prerequisites
|
|
29
31
|
|
|
30
32
|
1. **Bun 1.3+ (`bunx`) on PATH** - the pinned `@bnbagent/deploy-cli` runs through it.
|
|
31
33
|
2. **Docker running** - the image is built locally (linux/amd64) before push.
|
|
32
|
-
3. **An Azure subscription** the operator may provision in (Foundry account/project, container registry, hosted agent)
|
|
34
|
+
3. **An Azure subscription** the operator may provision in (Foundry account/project, container registry, hosted agent). Before a local self-deploy, run `bunx --bun @bnbagent/deploy-cli@0.5.10 login --provider azure`; use OIDC/service-principal credentials in CI.
|
|
33
35
|
|
|
34
36
|
## ⚠️ Foundry gotchas (read before deploying)
|
|
35
37
|
|
|
@@ -67,7 +69,7 @@ bag init myagent --runtime azure-foundry
|
|
|
67
69
|
|
|
68
70
|
```bash
|
|
69
71
|
bag deploy prepare --runtime azure-foundry # local readiness gate (region/subdomain/Dockerfile)
|
|
70
|
-
bag deploy --provider azure # delegated:
|
|
72
|
+
bag deploy --provider azure # delegated: onboard → build+push → CustomKeys → deploy [--smoke]
|
|
71
73
|
```
|
|
72
74
|
|
|
73
75
|
`bag deploy --provider azure` runs an HTTP contract smoke by default (pass `--skip-smoke` to omit it) and captures the Foundry endpoint into `app/agent/studio.toml [azure].agent_endpoint`. If Foundry creates the resource but that post-create check fails, Studio still records the discovered endpoint so `status`, `logs`, and `destroy` can manage the resource; the deploy command continues to return non-zero.
|
|
@@ -76,11 +78,11 @@ bag deploy --provider azure # delegated: login → onboard →
|
|
|
76
78
|
|
|
77
79
|
```bash
|
|
78
80
|
bag deploy status # all recorded providers + live state
|
|
79
|
-
bag deploy info --with-curl
|
|
81
|
+
bag deploy info --provider azure --with-curl # endpoint + Entra token shortcut + request body
|
|
80
82
|
bag deploy logs --provider azure --limit 50 # delegated Hosted Agent logs
|
|
81
83
|
```
|
|
82
84
|
|
|
83
|
-
`bag deploy info` reads the recorded Azure endpoint without recreating the
|
|
85
|
+
`bag deploy info --provider azure` reads the recorded Azure endpoint without recreating the
|
|
84
86
|
temporary deploy spec. `--with-curl` adds an optional `az account
|
|
85
87
|
get-access-token --resource https://ai.azure.com` shortcut and a complete
|
|
86
88
|
Invocations request. This does not make the Azure CLI a deploy prerequisite;
|
|
@@ -89,6 +91,31 @@ with `DefaultAzureCredential` or `ClientSecretCredential`.
|
|
|
89
91
|
|
|
90
92
|
The built-in smoke proves the container contract, not the seller signature. For a release E2E, invoke with a complete `negotiate` envelope and require `response.accepted=true`, a non-empty `negotiation_hash`, and `provider_sig`. The Invocations body is `{"input":"<serialized skill JSON>"}`.
|
|
91
93
|
|
|
94
|
+
### X402 external gateway
|
|
95
|
+
|
|
96
|
+
Foundry does not expose a raw anonymous `/x402` URL. An operator-run HTTPS
|
|
97
|
+
gateway authenticates to the recorded Invocations endpoint with an Entra token
|
|
98
|
+
for `https://ai.azure.com/.default`, wraps the incoming HTTP request as
|
|
99
|
+
`http-envelope-v1`, and unwraps the returned inner status, headers, and base64
|
|
100
|
+
body:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"v": 1,
|
|
105
|
+
"method": "POST",
|
|
106
|
+
"path": "/x402",
|
|
107
|
+
"query": {},
|
|
108
|
+
"headers": { "content-type": "application/json" },
|
|
109
|
+
"body": "<base64-encoded request body>"
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`bag deploy info --provider azure --json` reports the deployed face snapshot,
|
|
114
|
+
seller state, carrier contract, and a no-work route probe. A PAID seller also
|
|
115
|
+
needs fixed facilitator egress; use an Azure Container Apps workload-profiles
|
|
116
|
+
environment with VNet integration and a NAT Gateway static IP, then allowlist
|
|
117
|
+
that IP with B402. Studio does not deploy or manage the gateway or network.
|
|
118
|
+
|
|
92
119
|
### D. Tear down
|
|
93
120
|
|
|
94
121
|
```bash
|