@forgeax/game 0.3.0 → 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/README.md +45 -20
- package/assets/asset3d/provider/asset3d-search-provider-c181c48fbffc933a7ce9a0836f7878ca5e6d77e1-darwin-arm64.tar.gz +0 -0
- package/dist/main.js +335 -133
- package/docs/asset3d.md +48 -26
- package/docs/release-0.3.1.md +107 -0
- package/docs/release-0.3.2.md +90 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -54,12 +54,18 @@ unknown non-empty directories fail closed.
|
|
|
54
54
|
|
|
55
55
|
> [!NOTE]
|
|
56
56
|
> Asset3D is optional and **disabled by default**. The two commands above never contact
|
|
57
|
-
> the internal asset catalog,
|
|
57
|
+
> the internal asset catalog, provision the bundled platform Provider, or add
|
|
58
58
|
> `asset3d-search` to the project. Enable it only in an already initialized project:
|
|
59
59
|
>
|
|
60
60
|
> ```bash
|
|
61
61
|
> npx -y @forgeax/game asset3d enable
|
|
62
62
|
> ```
|
|
63
|
+
>
|
|
64
|
+
> With no override, `enable` uses the documented AW production service. Pass
|
|
65
|
+
> `--base-url <test-gateway-or-service-url>` only to select a test environment. The
|
|
66
|
+
> first enable prompts for the AW Sandbox Key with hidden input, validates it through
|
|
67
|
+
> the packaged Provider, and stores it outside the project in a private user
|
|
68
|
+
> credential file.
|
|
63
69
|
|
|
64
70
|
## Agent completion contract
|
|
65
71
|
|
|
@@ -151,29 +157,43 @@ a live unverifiable PID fails closed as `preview_ownership_unverified`.
|
|
|
151
157
|
|
|
152
158
|
## Asset3D installation and transaction
|
|
153
159
|
|
|
154
|
-
Asset3D is an opt-in, project-local MCP integration for
|
|
155
|
-
|
|
160
|
+
Asset3D is an opt-in, project-local MCP integration for AW HybridSearch. Run it only
|
|
161
|
+
after `init`, while connected to the internal network. The no-argument command uses
|
|
162
|
+
the documented production service:
|
|
156
163
|
|
|
157
164
|
```bash
|
|
158
165
|
npx -y @forgeax/game asset3d enable
|
|
159
166
|
forgeax-game asset3d doctor --json
|
|
160
167
|
```
|
|
161
168
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
169
|
+
The service selection precedence is explicit `--base-url`, then
|
|
170
|
+
`FORGEAX_ASSET_LIBRARY_BASE_URL`, then the built-in production service. An override
|
|
171
|
+
accepts either a gateway root or the full
|
|
172
|
+
`trpc.oasismetric.omcontentserver.http` service root. On first use, `enable` reads the
|
|
173
|
+
key with hidden TTY input. Non-interactive automation must inject
|
|
174
|
+
`FORGEAX_ASSET3D_AW_SANDBOX_KEY` from its secret store. The key is validated by the
|
|
175
|
+
Provider against the selected environment's `HybridSearch` before the command
|
|
176
|
+
reports success.
|
|
177
|
+
|
|
178
|
+
The validated key is stored in
|
|
179
|
+
`~/.forgeax/credentials/asset3d-aw.json` as an owned, non-symlink `0600` file. Project
|
|
180
|
+
`.forgeax/mcp.json` contains only the AW service URL and the credential-file path; it
|
|
181
|
+
never contains the key or a signed asset URL. The Provider access check returns only
|
|
182
|
+
the exact download origins required by the sandbox. Failed validation or installation
|
|
183
|
+
rolls back a newly written credential.
|
|
184
|
+
|
|
185
|
+
The npm package carries the digest-pinned Darwin arm64 Provider archive as an inert
|
|
186
|
+
asset. `enable` reuses the exact verified Provider from
|
|
187
|
+
`~/.forgeax/providers/asset3d-search/<sha256>/`, or provisions that packaged archive
|
|
188
|
+
when the cache is absent. It never downloads Provider code from GitHub or another
|
|
189
|
+
repository. Only after Provider verification and its MCP handshake succeed does it merge the `forgeax` and
|
|
168
190
|
`asset3d-search` entries into `.forgeax/mcp.json` and project the approved Asset3D
|
|
169
191
|
workflow into the Agent hosts selected during `install`.
|
|
170
192
|
|
|
171
193
|
> [!IMPORTANT]
|
|
172
|
-
>
|
|
173
|
-
>
|
|
174
|
-
>
|
|
175
|
-
> that service publishes an explicit authentication header and validation contract;
|
|
176
|
-
> an arbitrary secret is never guessed or persisted in project config.
|
|
194
|
+
> Asset3D uses the AW-specific `X-Sandbox-Key` contract. It never reads
|
|
195
|
+
> `ANTHROPIC_API_KEY` or another model-provider key. Do not pass keys as command-line
|
|
196
|
+
> arguments, commit the user credential file, or copy a key into project MCP config.
|
|
177
197
|
|
|
178
198
|
The transaction commands are normally driven by the installed Agent workflow:
|
|
179
199
|
|
|
@@ -191,18 +211,20 @@ forgeax-game asset3d uninstall
|
|
|
191
211
|
<details>
|
|
192
212
|
<summary>Maintainer-only local bundle installation</summary>
|
|
193
213
|
|
|
194
|
-
The low-level command remains available for producing and validating platform
|
|
214
|
+
The low-level command remains available for producing and validating platform bundle
|
|
195
215
|
artifacts. It is not the user onboarding path.
|
|
196
216
|
|
|
197
217
|
```bash
|
|
198
218
|
forgeax-game asset3d install \
|
|
199
219
|
--provider-bundle ./asset3d-search-provider-<commit>-<os>-<arch>.tar.gz \
|
|
200
220
|
--sha256 <archive-sha256> \
|
|
201
|
-
--download-origin
|
|
202
|
-
--
|
|
221
|
+
--download-origin https://<approved-download-host>:443 \
|
|
222
|
+
--aw-base-url <aw-gateway-or-service-url> \
|
|
223
|
+
--aw-credential-file /absolute/path/to/private-credential.json
|
|
203
224
|
```
|
|
204
225
|
|
|
205
|
-
|
|
226
|
+
`--catalog-base-url` remains an explicit compatibility-only adapter for the retired
|
|
227
|
+
directory service; normal users should not select it.
|
|
206
228
|
|
|
207
229
|
</details>
|
|
208
230
|
|
|
@@ -227,5 +249,8 @@ and evidence boundaries.
|
|
|
227
249
|
> acceptance gates.
|
|
228
250
|
|
|
229
251
|
See [docs/runtime.md](docs/runtime.md) for the exact artifact and lifecycle contract.
|
|
230
|
-
For the
|
|
231
|
-
checklist, see [docs/release-0.3.
|
|
252
|
+
For the current production-default environment contract and maintainer publish
|
|
253
|
+
checklist, see [docs/release-0.3.2.md](docs/release-0.3.2.md). The authenticated AW
|
|
254
|
+
Provider delivery is documented in [docs/release-0.3.1.md](docs/release-0.3.1.md),
|
|
255
|
+
and the original two-command onboarding contract remains documented in
|
|
256
|
+
[docs/release-0.3.0.md](docs/release-0.3.0.md).
|
|
Binary file
|
package/dist/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/main.ts
|
|
4
|
-
import { resolve as
|
|
4
|
+
import { resolve as resolve15 } from "node:path";
|
|
5
5
|
|
|
6
6
|
// src/mcp/protocol.ts
|
|
7
7
|
var MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
@@ -2946,7 +2946,7 @@ var RELEASE_IDENTITY_MIME = "application/vnd.forgeax.game-release-identity+json"
|
|
|
2946
2946
|
var RELEASE_IDENTITY = Object.freeze({
|
|
2947
2947
|
schema: RELEASE_IDENTITY_SCHEMA,
|
|
2948
2948
|
gamePackage: "@forgeax/game",
|
|
2949
|
-
gameVersion: "0.3.
|
|
2949
|
+
gameVersion: "0.3.2",
|
|
2950
2950
|
gameBin: "forgeax-game",
|
|
2951
2951
|
engineSdkPackage: ENGINE_SDK_PACKAGE,
|
|
2952
2952
|
engineSdkVersion: ENGINE_VERSION,
|
|
@@ -3226,9 +3226,9 @@ async function startHttpMcpServer(spec, options) {
|
|
|
3226
3226
|
}
|
|
3227
3227
|
|
|
3228
3228
|
// src/cli/dispatch.ts
|
|
3229
|
-
import { existsSync as
|
|
3230
|
-
import { arch as arch2, homedir as
|
|
3231
|
-
import { join as
|
|
3229
|
+
import { existsSync as existsSync12, readFileSync as readFileSync15, writeFileSync as writeFileSync11 } from "node:fs";
|
|
3230
|
+
import { arch as arch2, homedir as homedir5, platform as platform2 } from "node:os";
|
|
3231
|
+
import { join as join11, resolve as resolve14 } from "node:path";
|
|
3232
3232
|
|
|
3233
3233
|
// src/install/clients.ts
|
|
3234
3234
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -3355,7 +3355,7 @@ function launchSpec(mode) {
|
|
|
3355
3355
|
}
|
|
3356
3356
|
return {
|
|
3357
3357
|
command: "npx",
|
|
3358
|
-
args: ["-y", "-p", "@forgeax/game@0.3.
|
|
3358
|
+
args: ["-y", "-p", "@forgeax/game@0.3.2", "forgeax-game", "mcp"]
|
|
3359
3359
|
};
|
|
3360
3360
|
}
|
|
3361
3361
|
|
|
@@ -4164,6 +4164,7 @@ import {
|
|
|
4164
4164
|
chmodSync as chmodSync4,
|
|
4165
4165
|
copyFileSync as copyFileSync3,
|
|
4166
4166
|
existsSync as existsSync8,
|
|
4167
|
+
lstatSync as lstatSync7,
|
|
4167
4168
|
mkdirSync as mkdirSync9,
|
|
4168
4169
|
mkdtempSync as mkdtempSync2,
|
|
4169
4170
|
readFileSync as readFileSync12,
|
|
@@ -4186,12 +4187,11 @@ var PROVIDER_RECEIPT_SCHEMA = "forgeax.asset3d-search-receipt/1.0.0";
|
|
|
4186
4187
|
var K0_PACKAGE_SHA256 = "0c3fc5d448b94efadb7fe03ee48ef3d8964434c2789505297a99c3a2715dd83b";
|
|
4187
4188
|
var RESULT_SCHEMA_SHA256 = "ab6e5e1e794d5428efef362fe32d4c54b6541e12539bae5f6ff27680c9f380ba";
|
|
4188
4189
|
var RECEIPT_SCHEMA_SHA256 = "e0dc9e9fe9872f09af9fca6d0c17d22aca63c2b546c55c0b669c581f7059b9c4";
|
|
4189
|
-
var ASSET3D_PROVIDER_COMMIT = "
|
|
4190
|
-
var
|
|
4191
|
-
var DEFAULT_ASSET3D_PROVIDER_RELEASES = Object.freeze({
|
|
4190
|
+
var ASSET3D_PROVIDER_COMMIT = "c181c48fbffc933a7ce9a0836f7878ca5e6d77e1";
|
|
4191
|
+
var BUNDLED_ASSET3D_PROVIDERS = Object.freeze({
|
|
4192
4192
|
"darwin-arm64": {
|
|
4193
|
-
sha256: "
|
|
4194
|
-
|
|
4193
|
+
sha256: "c8d2afd7ba68ae4b3540c927a2ff7133a82ea90975d09cab1d10b003fbbdeed3",
|
|
4194
|
+
relativePath: "asset3d/provider/asset3d-search-provider-c181c48fbffc933a7ce9a0836f7878ca5e6d77e1-darwin-arm64.tar.gz"
|
|
4195
4195
|
}
|
|
4196
4196
|
});
|
|
4197
4197
|
var INSTALL_SCHEMA = "forgeax.asset3d-install/1.0.0";
|
|
@@ -4311,6 +4311,22 @@ var K0_FILES = [
|
|
|
4311
4311
|
"scripts/workflow.test.mjs"
|
|
4312
4312
|
];
|
|
4313
4313
|
var SKILL_ID = "art-3d-asset-library";
|
|
4314
|
+
function packagedAsset3dProvider(relativePathInput, assetsRootInput) {
|
|
4315
|
+
const assetsRoot = realpathSync7(assetsRootInput ?? defaultAssetsRoot());
|
|
4316
|
+
const relativePath = relativePathInput.replaceAll("\\", "/");
|
|
4317
|
+
if (!relativePath || relativePath.startsWith("/") || relativePath.split("/").some((part) => part === "" || part === "." || part === "..")) {
|
|
4318
|
+
throw new Error("asset3d_bundled_provider_path_invalid");
|
|
4319
|
+
}
|
|
4320
|
+
const candidate = resolve10(assetsRoot, relativePath);
|
|
4321
|
+
if (!confined3(assetsRoot, candidate) || !existsSync8(candidate)) {
|
|
4322
|
+
throw new Error("asset3d_bundled_provider_missing: reinstall @forgeax/game");
|
|
4323
|
+
}
|
|
4324
|
+
const metadata = lstatSync7(candidate);
|
|
4325
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
4326
|
+
throw new Error("asset3d_bundled_provider_invalid: expected a regular package file");
|
|
4327
|
+
}
|
|
4328
|
+
return realpathSync7(candidate);
|
|
4329
|
+
}
|
|
4314
4330
|
function confined3(root, candidate) {
|
|
4315
4331
|
const rel = relative8(root, candidate);
|
|
4316
4332
|
return rel === "" || !isAbsolute3(rel) && rel !== ".." && !rel.startsWith(`..${sep5}`);
|
|
@@ -4485,7 +4501,7 @@ function filesUnder2(root, directory = root) {
|
|
|
4485
4501
|
})();
|
|
4486
4502
|
});
|
|
4487
4503
|
}
|
|
4488
|
-
function launchEntries(projectRoot, providerCache, origins, gamePluginLaunch, catalogBaseUrl) {
|
|
4504
|
+
function launchEntries(projectRoot, providerCache, origins, gamePluginLaunch, catalogBaseUrl, awApiBaseUrl, awCredentialFile) {
|
|
4489
4505
|
const quarantine = resolve10(projectRoot, ".forgeax", "asset3d-quarantine");
|
|
4490
4506
|
const workspace = resolve10(quarantine, "workspace");
|
|
4491
4507
|
return {
|
|
@@ -4506,7 +4522,9 @@ function launchEntries(projectRoot, providerCache, origins, gamePluginLaunch, ca
|
|
|
4506
4522
|
MCP_WORKSPACE_ROOT: workspace,
|
|
4507
4523
|
MCP_GAME_RUNTIME_ROOT: resolve10(quarantine, "game-runtime"),
|
|
4508
4524
|
AW_DOWNLOAD_ORIGINS: origins.compactJson,
|
|
4509
|
-
...catalogBaseUrl ? { ASSET3D_CATALOG_BASE_URL: catalogBaseUrl } : {}
|
|
4525
|
+
...catalogBaseUrl ? { ASSET3D_CATALOG_BASE_URL: catalogBaseUrl } : {},
|
|
4526
|
+
...awApiBaseUrl ? { AW_API_BASE_URL: awApiBaseUrl } : {},
|
|
4527
|
+
...awCredentialFile ? { AW_API_CREDENTIAL_FILE: awCredentialFile } : {}
|
|
4510
4528
|
},
|
|
4511
4529
|
toolCallTimeoutsMs: { default: 30000, tools: { search_asset: 195000 } }
|
|
4512
4530
|
}
|
|
@@ -4676,6 +4694,14 @@ async function publishAsset3d(options, provisioned) {
|
|
|
4676
4694
|
if (catalogBaseUrl && !origins.values.includes(catalogBaseUrl)) {
|
|
4677
4695
|
throw new Error("catalog_origin_not_authorized: --catalog-base-url must also be a --download-origin");
|
|
4678
4696
|
}
|
|
4697
|
+
if (Boolean(options.awApiBaseUrl) !== Boolean(options.awCredentialFile)) {
|
|
4698
|
+
throw new Error("asset3d_aw_config_invalid: AW API base URL and credential file must be configured together");
|
|
4699
|
+
}
|
|
4700
|
+
const awApiBaseUrl = options.awApiBaseUrl;
|
|
4701
|
+
const awCredentialFile = options.awCredentialFile ? resolve10(options.awCredentialFile) : undefined;
|
|
4702
|
+
if (awCredentialFile && !isAbsolute3(options.awCredentialFile)) {
|
|
4703
|
+
throw new Error("asset3d_credential_path_invalid: absolute path required");
|
|
4704
|
+
}
|
|
4679
4705
|
const assetsRoot = options.assetsRoot ?? defaultAssetsRoot();
|
|
4680
4706
|
const asset3dAssets = existsSync8(resolve10(assetsRoot, "asset3d", "vibegame-art-3d-asset-library-2.0.0.tgz")) ? resolve10(assetsRoot, "asset3d") : assetsRoot;
|
|
4681
4707
|
const k0 = resolve10(asset3dAssets, "vibegame-art-3d-asset-library-2.0.0.tgz");
|
|
@@ -4687,7 +4713,7 @@ async function publishAsset3d(options, provisioned) {
|
|
|
4687
4713
|
command: realpathSync7(options.executable ?? process.argv[1]),
|
|
4688
4714
|
args: ["mcp"]
|
|
4689
4715
|
};
|
|
4690
|
-
const entries = launchEntries(projectRoot, provisioned.cache, origins, gamePluginLaunch, catalogBaseUrl);
|
|
4716
|
+
const entries = launchEntries(projectRoot, provisioned.cache, origins, gamePluginLaunch, catalogBaseUrl, awApiBaseUrl, awCredentialFile);
|
|
4691
4717
|
if (options.verifyMcp !== false)
|
|
4692
4718
|
await verifyProviderMcp(entries["asset3d-search"]);
|
|
4693
4719
|
const configPath = resolve10(forgeax, "mcp.json");
|
|
@@ -4731,6 +4757,13 @@ async function installAsset3d(options) {
|
|
|
4731
4757
|
const cacheRoot = resolve10(options.cacheRoot ?? join10(homedir3(), ".forgeax", "providers", "asset3d-search"));
|
|
4732
4758
|
return publishAsset3d(options, provisionBundle(archive, options.expectedSha256, cacheRoot, options.python));
|
|
4733
4759
|
}
|
|
4760
|
+
function prepareAsset3dProvider(options) {
|
|
4761
|
+
if (/^https?:/i.test(options.providerBundle))
|
|
4762
|
+
throw new Error("asset3d_remote_artifact_unsupported");
|
|
4763
|
+
const archive = realpathSync7(options.providerBundle);
|
|
4764
|
+
const cacheRoot = resolve10(options.cacheRoot ?? join10(homedir3(), ".forgeax", "providers", "asset3d-search"));
|
|
4765
|
+
return provisionBundle(archive, options.expectedSha256, cacheRoot, options.python).cache;
|
|
4766
|
+
}
|
|
4734
4767
|
async function installProvisionedAsset3d(options) {
|
|
4735
4768
|
return publishAsset3d(options, useProvisionedCache(options.providerCache, options.expectedSha256));
|
|
4736
4769
|
}
|
|
@@ -5406,93 +5439,209 @@ function doctorAsset3d(projectRootInput, options = {}) {
|
|
|
5406
5439
|
return { installed: true, recovered, engine: { version: release.version, commit: release.commit }, provider: { commit: install.providerCommit, cache: install.providerCache, live: existsSync9(resolve11(install.providerCache, "venv", "bin", "python")) } };
|
|
5407
5440
|
}
|
|
5408
5441
|
|
|
5409
|
-
// src/asset3d/
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5442
|
+
// src/asset3d/aw-access.ts
|
|
5443
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
5444
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
5445
|
+
import { resolve as resolve12 } from "node:path";
|
|
5446
|
+
var AW_SERVICE_PATH = "/trpc.oasismetric.omcontentserver.http";
|
|
5447
|
+
var ACCESS_CHECK_SCHEMA = "forgeax.asset3d-access-check/1.0.0";
|
|
5448
|
+
var DEFAULT_AW_PRODUCTION_SERVICE_ROOT = "http://lb-pl74wsqg-5wi8ujmy1fq2746r.clb.usw-tencentclb.com:8008/trpc.oasismetric.omcontentserver.http";
|
|
5449
|
+
function normalizeAwServiceRoot(input) {
|
|
5450
|
+
let parsed;
|
|
5415
5451
|
try {
|
|
5416
|
-
|
|
5417
|
-
method: "HEAD",
|
|
5418
|
-
redirect: "manual",
|
|
5419
|
-
signal: controller.signal
|
|
5420
|
-
})).status;
|
|
5452
|
+
parsed = new URL(input);
|
|
5421
5453
|
} catch {
|
|
5422
|
-
throw new Error("
|
|
5423
|
-
}
|
|
5424
|
-
|
|
5454
|
+
throw new Error("asset3d_base_url_invalid: expected an HTTP(S) AW gateway or service URL");
|
|
5455
|
+
}
|
|
5456
|
+
if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
5457
|
+
throw new Error("asset3d_base_url_invalid: credentials, query, and fragment are forbidden");
|
|
5458
|
+
}
|
|
5459
|
+
let path = parsed.pathname.replace(/\/+$/, "");
|
|
5460
|
+
if (path.endsWith(`${AW_SERVICE_PATH}/HybridSearch`))
|
|
5461
|
+
path = path.slice(0, -"/HybridSearch".length);
|
|
5462
|
+
else if (!path.endsWith(AW_SERVICE_PATH))
|
|
5463
|
+
path = `${path}${AW_SERVICE_PATH}`;
|
|
5464
|
+
parsed.pathname = path;
|
|
5465
|
+
return parsed.toString().replace(/\/$/, "");
|
|
5466
|
+
}
|
|
5467
|
+
function resolveAwServiceRoot(explicit) {
|
|
5468
|
+
const configured = explicit || process.env.FORGEAX_ASSET_LIBRARY_BASE_URL || DEFAULT_AW_PRODUCTION_SERVICE_ROOT;
|
|
5469
|
+
return normalizeAwServiceRoot(configured);
|
|
5470
|
+
}
|
|
5471
|
+
function checkAwProviderAccess(options) {
|
|
5472
|
+
const command = resolve12(options.providerCache, "bin", "asset3d-search");
|
|
5473
|
+
if (!existsSync10(command))
|
|
5474
|
+
throw new Error("asset3d_provider_not_prepared");
|
|
5475
|
+
const result = spawnSync5(command, ["--check-aw-access"], {
|
|
5476
|
+
encoding: "utf8",
|
|
5477
|
+
timeout: 45000,
|
|
5478
|
+
maxBuffer: 128 * 1024,
|
|
5479
|
+
env: {
|
|
5480
|
+
...process.env,
|
|
5481
|
+
ASSET3D_CATALOG_BASE_URL: "",
|
|
5482
|
+
AW_API_BASE_URL: options.serviceRoot,
|
|
5483
|
+
AW_API_CREDENTIAL_FILE: resolve12(options.credentialFile),
|
|
5484
|
+
AW_API_SANDBOX_KEY: ""
|
|
5485
|
+
}
|
|
5486
|
+
});
|
|
5487
|
+
let payload;
|
|
5488
|
+
try {
|
|
5489
|
+
payload = JSON.parse(result.stdout);
|
|
5490
|
+
} catch {
|
|
5491
|
+
throw new Error("asset3d_access_validation_failed: Provider returned an invalid response");
|
|
5425
5492
|
}
|
|
5426
|
-
if (
|
|
5427
|
-
|
|
5428
|
-
if (status === 401 || status === 403) {
|
|
5429
|
-
throw new Error("asset3d_api_key_contract_missing: the catalog requires authentication but does not publish a supported key contract");
|
|
5493
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
5494
|
+
throw new Error("asset3d_access_validation_failed: Provider returned an invalid response");
|
|
5430
5495
|
}
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
import { createHash as createHash8 } from "node:crypto";
|
|
5436
|
-
import { mkdtempSync as mkdtempSync3, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "node:fs";
|
|
5437
|
-
import { tmpdir as tmpdir3 } from "node:os";
|
|
5438
|
-
import { basename as basename4, join as join11 } from "node:path";
|
|
5439
|
-
var MAX_PROVIDER_ARCHIVE_BYTES = 256 * 1024 * 1024;
|
|
5440
|
-
async function downloadProviderRelease(release, request2 = fetch) {
|
|
5441
|
-
if (!/^[a-f0-9]{64}$/.test(release.sha256))
|
|
5442
|
-
throw new Error("provider_bundle_digest_invalid");
|
|
5443
|
-
let url = new URL(release.url);
|
|
5444
|
-
if (url.protocol !== "https:")
|
|
5445
|
-
throw new Error("asset3d_provider_release_url_invalid: HTTPS is required");
|
|
5446
|
-
let response;
|
|
5447
|
-
for (let redirects = 0;redirects <= 5; redirects++) {
|
|
5448
|
-
response = await request2(url, { method: "GET", redirect: "manual" });
|
|
5449
|
-
if (![301, 302, 303, 307, 308].includes(response.status))
|
|
5450
|
-
break;
|
|
5451
|
-
const location = response.headers.get("location");
|
|
5452
|
-
if (!location)
|
|
5453
|
-
throw new Error("asset3d_provider_download_failed: redirect has no location");
|
|
5454
|
-
url = new URL(location, url);
|
|
5455
|
-
if (url.protocol !== "https:")
|
|
5456
|
-
throw new Error("asset3d_provider_download_failed: HTTPS downgrade rejected");
|
|
5457
|
-
response = undefined;
|
|
5496
|
+
const envelope = payload;
|
|
5497
|
+
if (result.status !== 0 || envelope.ok !== true) {
|
|
5498
|
+
const code = envelope.error?.code === "asset3d_access_validation_inconclusive" ? "asset3d_access_validation_inconclusive" : "asset3d_api_key_invalid_or_unavailable";
|
|
5499
|
+
throw new Error(`${code}: verify the AW service address, internal network, and Sandbox Key`);
|
|
5458
5500
|
}
|
|
5459
|
-
if (
|
|
5460
|
-
throw new Error(
|
|
5501
|
+
if (envelope.schemaVersion !== ACCESS_CHECK_SCHEMA || envelope.value?.authentication !== "sandbox-key" || !Array.isArray(envelope.value.downloadOrigins)) {
|
|
5502
|
+
throw new Error("asset3d_access_validation_failed: Provider returned an invalid response");
|
|
5461
5503
|
}
|
|
5462
|
-
const
|
|
5463
|
-
|
|
5464
|
-
|
|
5504
|
+
const origins = canonicalizeOrigins(envelope.value.downloadOrigins).values;
|
|
5505
|
+
return { serviceRoot: options.serviceRoot, authentication: "sandbox-key", downloadOrigins: origins };
|
|
5506
|
+
}
|
|
5507
|
+
|
|
5508
|
+
// src/asset3d/credentials.ts
|
|
5509
|
+
import { chmodSync as chmodSync5, existsSync as existsSync11, lstatSync as lstatSync9, readFileSync as readFileSync14, statSync as statSync7, unlinkSync as unlinkSync4 } from "node:fs";
|
|
5510
|
+
import { homedir as homedir4 } from "node:os";
|
|
5511
|
+
import { dirname as dirname11, isAbsolute as isAbsolute5, resolve as resolve13 } from "node:path";
|
|
5512
|
+
var AW_CREDENTIAL_SCHEMA = "forgeax.asset3d-credential/1.0.0";
|
|
5513
|
+
var AW_KEY_ENV = "FORGEAX_ASSET3D_AW_SANDBOX_KEY";
|
|
5514
|
+
var MAX_CREDENTIAL_BYTES = 4096;
|
|
5515
|
+
function validateKey(value) {
|
|
5516
|
+
if (!value || value.length > 2048 || [...value].some((character) => {
|
|
5517
|
+
const code = character.charCodeAt(0);
|
|
5518
|
+
return code < 32 || code === 127;
|
|
5519
|
+
})) {
|
|
5520
|
+
throw new Error("asset3d_api_key_invalid: expected a non-empty printable key");
|
|
5465
5521
|
}
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5522
|
+
return value;
|
|
5523
|
+
}
|
|
5524
|
+
function defaultAwCredentialFile() {
|
|
5525
|
+
const configured = process.env.FORGEAX_ASSET3D_CREDENTIAL_FILE;
|
|
5526
|
+
return resolve13(configured || resolve13(homedir4(), ".forgeax", "credentials", "asset3d-aw.json"));
|
|
5527
|
+
}
|
|
5528
|
+
function readAwCredential(pathInput) {
|
|
5529
|
+
const path = resolve13(pathInput);
|
|
5530
|
+
if (!isAbsolute5(pathInput))
|
|
5531
|
+
throw new Error("asset3d_credential_path_invalid: absolute path required");
|
|
5532
|
+
if (!existsSync11(path))
|
|
5533
|
+
return;
|
|
5534
|
+
try {
|
|
5535
|
+
const metadata = lstatSync9(path);
|
|
5536
|
+
const wrongOwner = typeof process.getuid === "function" && metadata.uid !== process.getuid();
|
|
5537
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || wrongOwner)
|
|
5538
|
+
throw new Error;
|
|
5539
|
+
if ((metadata.mode & 63) !== 0 || metadata.size < 1 || metadata.size > MAX_CREDENTIAL_BYTES)
|
|
5540
|
+
throw new Error;
|
|
5541
|
+
const parsed = JSON.parse(readFileSync14(path, "utf8"));
|
|
5542
|
+
if (Object.keys(parsed).sort().join(",") !== "provider,sandboxKey,schemaVersion")
|
|
5543
|
+
throw new Error;
|
|
5544
|
+
if (parsed.schemaVersion !== AW_CREDENTIAL_SCHEMA || parsed.provider !== "aw" || typeof parsed.sandboxKey !== "string")
|
|
5545
|
+
throw new Error;
|
|
5546
|
+
return validateKey(parsed.sandboxKey);
|
|
5547
|
+
} catch {
|
|
5548
|
+
throw new Error("asset3d_credential_invalid: credential file must be an owned 0600 regular file with the supported schema");
|
|
5481
5549
|
}
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
if (
|
|
5485
|
-
throw new Error(`
|
|
5550
|
+
}
|
|
5551
|
+
async function promptAwKey() {
|
|
5552
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || typeof process.stdin.setRawMode !== "function") {
|
|
5553
|
+
throw new Error(`asset3d_api_key_required: set ${AW_KEY_ENV} or rerun in an interactive terminal`);
|
|
5486
5554
|
}
|
|
5487
|
-
|
|
5488
|
-
const
|
|
5555
|
+
process.stdout.write("AW Asset3D Sandbox Key (input hidden): ");
|
|
5556
|
+
const input = process.stdin;
|
|
5557
|
+
const previousRaw = input.isRaw;
|
|
5558
|
+
input.setRawMode(true);
|
|
5559
|
+
input.resume();
|
|
5560
|
+
input.setEncoding("utf8");
|
|
5489
5561
|
try {
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5562
|
+
const value = await new Promise((resolveValue, reject) => {
|
|
5563
|
+
let collected = "";
|
|
5564
|
+
const onData = (chunk) => {
|
|
5565
|
+
for (const character of chunk) {
|
|
5566
|
+
if (character === "\x03") {
|
|
5567
|
+
input.off("data", onData);
|
|
5568
|
+
reject(new Error("asset3d_api_key_input_cancelled"));
|
|
5569
|
+
return;
|
|
5570
|
+
}
|
|
5571
|
+
if (character === "\r" || character === `
|
|
5572
|
+
`) {
|
|
5573
|
+
input.off("data", onData);
|
|
5574
|
+
resolveValue(collected);
|
|
5575
|
+
return;
|
|
5576
|
+
}
|
|
5577
|
+
if (character === "" || character === "\b")
|
|
5578
|
+
collected = collected.slice(0, -1);
|
|
5579
|
+
else
|
|
5580
|
+
collected += character;
|
|
5581
|
+
}
|
|
5582
|
+
};
|
|
5583
|
+
input.on("data", onData);
|
|
5584
|
+
});
|
|
5585
|
+
process.stdout.write(`
|
|
5586
|
+
`);
|
|
5587
|
+
return validateKey(value);
|
|
5588
|
+
} finally {
|
|
5589
|
+
input.setRawMode(previousRaw ?? false);
|
|
5590
|
+
input.pause();
|
|
5591
|
+
}
|
|
5592
|
+
}
|
|
5593
|
+
async function acquireAwKey(path) {
|
|
5594
|
+
const fromEnvironment = process.env[AW_KEY_ENV];
|
|
5595
|
+
if (fromEnvironment)
|
|
5596
|
+
return { key: validateKey(fromEnvironment), source: "environment" };
|
|
5597
|
+
const stored = readAwCredential(path);
|
|
5598
|
+
if (stored)
|
|
5599
|
+
return { key: stored, source: "stored" };
|
|
5600
|
+
return { key: await promptAwKey(), source: "prompt" };
|
|
5601
|
+
}
|
|
5602
|
+
function writeAwCredential(pathInput, keyInput) {
|
|
5603
|
+
if (!isAbsolute5(pathInput))
|
|
5604
|
+
throw new Error("asset3d_credential_path_invalid: absolute path required");
|
|
5605
|
+
const path = resolve13(pathInput);
|
|
5606
|
+
const key = validateKey(keyInput);
|
|
5607
|
+
const parent = dirname11(path);
|
|
5608
|
+
ensurePrivateDir(parent);
|
|
5609
|
+
const parentMetadata = lstatSync9(parent);
|
|
5610
|
+
const wrongParentOwner = typeof process.getuid === "function" && parentMetadata.uid !== process.getuid();
|
|
5611
|
+
if (!parentMetadata.isDirectory() || parentMetadata.isSymbolicLink() || wrongParentOwner) {
|
|
5612
|
+
throw new Error("asset3d_credential_path_invalid: parent must be an owned regular directory");
|
|
5613
|
+
}
|
|
5614
|
+
chmodSync5(parent, 448);
|
|
5615
|
+
const existed = existsSync11(path);
|
|
5616
|
+
if (existed)
|
|
5617
|
+
readAwCredential(path);
|
|
5618
|
+
const previous = existed ? readFileSync14(path) : undefined;
|
|
5619
|
+
const previousMode = existed ? statSync7(path).mode & 511 : undefined;
|
|
5620
|
+
const bytes = `${JSON.stringify({ schemaVersion: AW_CREDENTIAL_SCHEMA, provider: "aw", sandboxKey: key }, null, 2)}
|
|
5621
|
+
`;
|
|
5622
|
+
const changed = !previous || !previous.equals(Buffer.from(bytes));
|
|
5623
|
+
if (changed)
|
|
5624
|
+
atomicWrite(path, bytes, 384);
|
|
5625
|
+
chmodSync5(path, 384);
|
|
5626
|
+
let active = true;
|
|
5627
|
+
return {
|
|
5628
|
+
path,
|
|
5629
|
+
changed,
|
|
5630
|
+
commit() {
|
|
5631
|
+
active = false;
|
|
5632
|
+
},
|
|
5633
|
+
rollback() {
|
|
5634
|
+
if (!active || !changed)
|
|
5635
|
+
return;
|
|
5636
|
+
if (previous) {
|
|
5637
|
+
atomicWrite(path, previous, previousMode ?? 384);
|
|
5638
|
+
chmodSync5(path, previousMode ?? 384);
|
|
5639
|
+
} else {
|
|
5640
|
+
unlinkSync4(path);
|
|
5641
|
+
}
|
|
5642
|
+
active = false;
|
|
5643
|
+
}
|
|
5644
|
+
};
|
|
5496
5645
|
}
|
|
5497
5646
|
|
|
5498
5647
|
// src/cli/dispatch.ts
|
|
@@ -5507,8 +5656,8 @@ Usage:
|
|
|
5507
5656
|
forgeax-game preview stop [--game <slug>] [--target-dir <path>] [--json]
|
|
5508
5657
|
forgeax-game devkit install
|
|
5509
5658
|
forgeax-game agents update
|
|
5510
|
-
forgeax-game asset3d enable [--ide ${CLIENT_CHOICES.join(",")}]
|
|
5511
|
-
forgeax-game asset3d install --provider-bundle <archive> --sha256 <hex> --download-origin <scheme://host:port> [--
|
|
5659
|
+
forgeax-game asset3d enable [--base-url <AW gateway/service URL>] [--ide ${CLIENT_CHOICES.join(",")}]
|
|
5660
|
+
forgeax-game asset3d install --provider-bundle <archive> --sha256 <hex> --download-origin <scheme://host:port> [--aw-base-url <URL> --aw-credential-file <absolute path>] [...]
|
|
5512
5661
|
forgeax-game asset3d uninstall
|
|
5513
5662
|
forgeax-game asset3d begin --query <text> [--query <text> ...] --json
|
|
5514
5663
|
forgeax-game asset3d commit --execution <uuid> --provider-result-stdin --json [--refresh]
|
|
@@ -5560,23 +5709,23 @@ function requireProject() {
|
|
|
5560
5709
|
return project.root;
|
|
5561
5710
|
}
|
|
5562
5711
|
function updateAgentsFile(root) {
|
|
5563
|
-
const path =
|
|
5564
|
-
const existing =
|
|
5712
|
+
const path = join11(root, "AGENTS.md");
|
|
5713
|
+
const existing = existsSync12(path) ? readFileSync15(path, "utf8") : undefined;
|
|
5565
5714
|
const content = upsertBlock(existing, ROUTING_TEXT);
|
|
5566
5715
|
if (content === existing)
|
|
5567
5716
|
return { path, changed: false };
|
|
5568
|
-
|
|
5717
|
+
writeFileSync11(path, content);
|
|
5569
5718
|
return { path, changed: true };
|
|
5570
5719
|
}
|
|
5571
5720
|
function removeAgentsBlock(root) {
|
|
5572
|
-
const path =
|
|
5573
|
-
if (!
|
|
5721
|
+
const path = join11(root, "AGENTS.md");
|
|
5722
|
+
if (!existsSync12(path))
|
|
5574
5723
|
return { path, changed: false };
|
|
5575
|
-
const existing =
|
|
5724
|
+
const existing = readFileSync15(path, "utf8");
|
|
5576
5725
|
const content = removeBlock(existing);
|
|
5577
5726
|
if (content === existing)
|
|
5578
5727
|
return { path, changed: false };
|
|
5579
|
-
|
|
5728
|
+
writeFileSync11(path, content);
|
|
5580
5729
|
return { path, changed: true };
|
|
5581
5730
|
}
|
|
5582
5731
|
async function installCommand(args) {
|
|
@@ -5675,7 +5824,7 @@ async function useCommand(args) {
|
|
|
5675
5824
|
throw new Error(`game ${JSON.stringify(slug)} not found. Available: ${listGames(root).join(", ") || "(none)"}`);
|
|
5676
5825
|
}
|
|
5677
5826
|
if (listGames(root).length > 1) {
|
|
5678
|
-
|
|
5827
|
+
writeFileSync11(join11(root, ".forgeax", "active-game.json"), `${JSON.stringify({ version: 1, slug }, null, 2)}
|
|
5679
5828
|
`, "utf8");
|
|
5680
5829
|
}
|
|
5681
5830
|
process.stdout.write(`Active game: ${slug}
|
|
@@ -5777,7 +5926,7 @@ async function uninstallCommand(args) {
|
|
|
5777
5926
|
const agents = removeAgentsBlock(root);
|
|
5778
5927
|
process.stdout.write(`${agents.changed ? "REMOVED" : "ABSENT "} routing block: ${agents.path}
|
|
5779
5928
|
`);
|
|
5780
|
-
process.stdout.write(`KEPT your games and project metadata: ${
|
|
5929
|
+
process.stdout.write(`KEPT your games and project metadata: ${join11(root, ".forgeax")}
|
|
5781
5930
|
`);
|
|
5782
5931
|
} else {
|
|
5783
5932
|
process.stdout.write(`INFO no ForgeaX project bound; only client configuration was touched.
|
|
@@ -6007,54 +6156,93 @@ function canonicalAsset3dClients(value) {
|
|
|
6007
6156
|
throw new Error(`asset3d_client_invalid: ${invalid.join(", ")}`);
|
|
6008
6157
|
return [...new Set(values)];
|
|
6009
6158
|
}
|
|
6159
|
+
function parseAsset3dEnableArgs(args) {
|
|
6160
|
+
const ideArgs = [];
|
|
6161
|
+
let baseUrl;
|
|
6162
|
+
for (let index = 0;index < args.length; index++) {
|
|
6163
|
+
const arg = args[index];
|
|
6164
|
+
if (arg === "--base-url") {
|
|
6165
|
+
const value = args[++index];
|
|
6166
|
+
if (!value)
|
|
6167
|
+
throw new Error("--base-url requires an AW gateway/service URL");
|
|
6168
|
+
baseUrl = value;
|
|
6169
|
+
continue;
|
|
6170
|
+
}
|
|
6171
|
+
if (arg.startsWith("--base-url=")) {
|
|
6172
|
+
baseUrl = arg.slice("--base-url=".length);
|
|
6173
|
+
continue;
|
|
6174
|
+
}
|
|
6175
|
+
if (arg === "--ide") {
|
|
6176
|
+
const value = args[++index];
|
|
6177
|
+
if (!value)
|
|
6178
|
+
throw new Error("--ide requires a comma-separated client list");
|
|
6179
|
+
ideArgs.push("--ide", value);
|
|
6180
|
+
continue;
|
|
6181
|
+
}
|
|
6182
|
+
if (arg.startsWith("--ide=")) {
|
|
6183
|
+
ideArgs.push(arg);
|
|
6184
|
+
continue;
|
|
6185
|
+
}
|
|
6186
|
+
throw new Error("usage: forgeax-game asset3d enable [--base-url <AW gateway/service URL>] [--ide codex,claude,cursor,...]");
|
|
6187
|
+
}
|
|
6188
|
+
return { requested: parseIdeSelector(ideArgs, "invalid --ide selector"), baseUrl };
|
|
6189
|
+
}
|
|
6010
6190
|
async function asset3dCommand(args) {
|
|
6011
6191
|
const [operation, ...rest] = args;
|
|
6012
6192
|
const projectRoot = requireProject();
|
|
6013
6193
|
try {
|
|
6014
6194
|
if (operation === "enable") {
|
|
6015
|
-
const
|
|
6016
|
-
const selection = selectClients(projectRoot, requested);
|
|
6195
|
+
const parsed = parseAsset3dEnableArgs(rest);
|
|
6196
|
+
const selection = selectClients(projectRoot, parsed.requested);
|
|
6017
6197
|
reportMissingClients(selection.missing);
|
|
6018
6198
|
if (selection.selected.length === 0) {
|
|
6019
6199
|
throw new Error("asset3d_client_missing: run `forgeax-game install --ide <client>` before enabling Asset3D");
|
|
6020
6200
|
}
|
|
6021
|
-
const
|
|
6201
|
+
const serviceRoot = resolveAwServiceRoot(parsed.baseUrl);
|
|
6022
6202
|
const os = platform2() === "win32" ? "windows" : platform2();
|
|
6023
6203
|
const cpu = arch2();
|
|
6024
6204
|
const target = `${os}-${cpu}`;
|
|
6025
|
-
const
|
|
6026
|
-
if (!
|
|
6205
|
+
const bundled = BUNDLED_ASSET3D_PROVIDERS[target];
|
|
6206
|
+
if (!bundled)
|
|
6027
6207
|
throw new Error(`asset3d_provider_target_unreleased: ${target}`);
|
|
6028
|
-
const providerCache =
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
expectedSha256:
|
|
6033
|
-
downloadOrigins: [access.origin],
|
|
6034
|
-
catalogBaseUrl: access.origin,
|
|
6035
|
-
clients,
|
|
6036
|
-
gamePluginLaunch: launchSpec("npx"),
|
|
6037
|
-
replaceOwned: true
|
|
6038
|
-
};
|
|
6039
|
-
let result;
|
|
6040
|
-
if (existsSync10(providerCache)) {
|
|
6041
|
-
result = await installProvisionedAsset3d({ ...common, providerCache });
|
|
6042
|
-
} else {
|
|
6043
|
-
const downloaded = await downloadProviderRelease(release);
|
|
6044
|
-
try {
|
|
6045
|
-
result = await installAsset3d({ ...common, providerBundle: downloaded.archive });
|
|
6046
|
-
} finally {
|
|
6047
|
-
downloaded.cleanup();
|
|
6048
|
-
}
|
|
6208
|
+
const providerCache = resolve14(homedir5(), ".forgeax", "providers", "asset3d-search", bundled.sha256);
|
|
6209
|
+
let preparedCache = providerCache;
|
|
6210
|
+
if (!existsSync12(providerCache)) {
|
|
6211
|
+
const providerBundle = packagedAsset3dProvider(bundled.relativePath);
|
|
6212
|
+
preparedCache = prepareAsset3dProvider({ providerBundle, expectedSha256: bundled.sha256 });
|
|
6049
6213
|
}
|
|
6050
|
-
|
|
6214
|
+
const credentialFile = defaultAwCredentialFile();
|
|
6215
|
+
const credential = await acquireAwKey(credentialFile);
|
|
6216
|
+
const credentialWrite = writeAwCredential(credentialFile, credential.key);
|
|
6217
|
+
const clients = selection.selected.filter((id) => (id in SKILL_MOUNTS));
|
|
6218
|
+
try {
|
|
6219
|
+
const access = checkAwProviderAccess({ providerCache: preparedCache, serviceRoot, credentialFile });
|
|
6220
|
+
const result = await installProvisionedAsset3d({
|
|
6221
|
+
projectRoot,
|
|
6222
|
+
providerCache: preparedCache,
|
|
6223
|
+
expectedSha256: bundled.sha256,
|
|
6224
|
+
downloadOrigins: access.downloadOrigins,
|
|
6225
|
+
awApiBaseUrl: access.serviceRoot,
|
|
6226
|
+
awCredentialFile: credentialFile,
|
|
6227
|
+
clients,
|
|
6228
|
+
gamePluginLaunch: launchSpec("npx"),
|
|
6229
|
+
replaceOwned: true
|
|
6230
|
+
});
|
|
6231
|
+
credentialWrite.commit();
|
|
6232
|
+
process.stdout.write(`${result.changed ? "ENABLED" : "CURRENT"} Asset3D provider ${result.providerCommit}; service=${access.serviceRoot}; authentication=${access.authentication}; skillFiles=${result.skillFiles}
|
|
6051
6233
|
`);
|
|
6052
|
-
|
|
6234
|
+
return 0;
|
|
6235
|
+
} catch (error) {
|
|
6236
|
+
credentialWrite.rollback();
|
|
6237
|
+
throw error;
|
|
6238
|
+
}
|
|
6053
6239
|
}
|
|
6054
6240
|
if (operation === "install") {
|
|
6055
6241
|
let providerBundle;
|
|
6056
6242
|
let expectedSha256;
|
|
6057
6243
|
let catalogBaseUrl;
|
|
6244
|
+
let awApiBaseUrl;
|
|
6245
|
+
let awCredentialFile;
|
|
6058
6246
|
const downloadOrigins = [];
|
|
6059
6247
|
let clients;
|
|
6060
6248
|
let replaceOwned = false;
|
|
@@ -6064,7 +6252,7 @@ async function asset3dCommand(args) {
|
|
|
6064
6252
|
replaceOwned = true;
|
|
6065
6253
|
continue;
|
|
6066
6254
|
}
|
|
6067
|
-
if (arg === "--provider-bundle" || arg === "--sha256" || arg === "--download-origin" || arg === "--catalog-base-url" || arg === "--ide") {
|
|
6255
|
+
if (arg === "--provider-bundle" || arg === "--sha256" || arg === "--download-origin" || arg === "--catalog-base-url" || arg === "--aw-base-url" || arg === "--aw-credential-file" || arg === "--ide") {
|
|
6068
6256
|
const value = rest[++index];
|
|
6069
6257
|
if (!value)
|
|
6070
6258
|
throw new Error(`${arg} requires a value`);
|
|
@@ -6076,6 +6264,10 @@ async function asset3dCommand(args) {
|
|
|
6076
6264
|
downloadOrigins.push(value);
|
|
6077
6265
|
else if (arg === "--catalog-base-url")
|
|
6078
6266
|
catalogBaseUrl = value;
|
|
6267
|
+
else if (arg === "--aw-base-url")
|
|
6268
|
+
awApiBaseUrl = resolveAwServiceRoot(value);
|
|
6269
|
+
else if (arg === "--aw-credential-file")
|
|
6270
|
+
awCredentialFile = resolve14(value);
|
|
6079
6271
|
else
|
|
6080
6272
|
clients = canonicalAsset3dClients(value);
|
|
6081
6273
|
continue;
|
|
@@ -6084,7 +6276,17 @@ async function asset3dCommand(args) {
|
|
|
6084
6276
|
}
|
|
6085
6277
|
if (!providerBundle || !expectedSha256)
|
|
6086
6278
|
throw new Error("usage: forgeax-game asset3d install --provider-bundle <archive> --sha256 <hex> --download-origin <origin> [...]");
|
|
6087
|
-
const result = await installAsset3d({
|
|
6279
|
+
const result = await installAsset3d({
|
|
6280
|
+
projectRoot,
|
|
6281
|
+
providerBundle,
|
|
6282
|
+
expectedSha256,
|
|
6283
|
+
downloadOrigins,
|
|
6284
|
+
...catalogBaseUrl ? { catalogBaseUrl } : {},
|
|
6285
|
+
...awApiBaseUrl ? { awApiBaseUrl } : {},
|
|
6286
|
+
...awCredentialFile ? { awCredentialFile } : {},
|
|
6287
|
+
...clients ? { clients } : {},
|
|
6288
|
+
replaceOwned
|
|
6289
|
+
});
|
|
6088
6290
|
process.stdout.write(`${result.changed ? "INSTALLED" : "CURRENT"} Asset3D provider ${result.providerCommit}; originSetDigest=${result.originSetDigest}; skillFiles=${result.skillFiles}
|
|
6089
6291
|
`);
|
|
6090
6292
|
return 0;
|
|
@@ -6259,7 +6461,7 @@ function parseMcpArgs(args) {
|
|
|
6259
6461
|
} else
|
|
6260
6462
|
throw new Error(`unknown MCP option: ${arg}`);
|
|
6261
6463
|
}
|
|
6262
|
-
return { transport, host, port, root:
|
|
6464
|
+
return { transport, host, port, root: resolve15(root), requireAuth, allowedOrigins };
|
|
6263
6465
|
}
|
|
6264
6466
|
async function runMcp(args) {
|
|
6265
6467
|
const options = parseMcpArgs(args);
|
package/docs/asset3d.md
CHANGED
|
@@ -13,19 +13,24 @@ second importer or Preview server.
|
|
|
13
13
|
> inside an already initialized Engine game.
|
|
14
14
|
|
|
15
15
|
> [!IMPORTANT]
|
|
16
|
-
> Provider archives are platform-specific
|
|
17
|
-
>
|
|
16
|
+
> Provider archives are platform-specific artifacts. The npm package embeds the
|
|
17
|
+
> currently released Darwin arm64 archive so a cache-empty user does not depend on a
|
|
18
|
+
> private source repository or release download. Installation deliberately returns
|
|
18
19
|
> `provider_platform_mismatch` when an archive target differs from the current host.
|
|
19
|
-
> Each
|
|
20
|
+
> Each bundled target therefore needs its own digest, offline verification, MCP
|
|
20
21
|
> handshake, and real service-path evidence.
|
|
21
22
|
|
|
22
23
|
## Trust and data flow
|
|
23
24
|
|
|
24
25
|
```mermaid
|
|
25
26
|
flowchart LR
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
U["Hidden key input or automation secret"] --> CRED["Private user credential file"]
|
|
28
|
+
B["Provider archive bundled in npm"] --> C["Managed digest cache"]
|
|
29
|
+
PKG["Exact K0 workflow TGZ"] --> S["Nine project Skill mounts"]
|
|
28
30
|
C --> M["Project-local stdio MCP"]
|
|
31
|
+
CRED --> M
|
|
32
|
+
M --> H["AW HybridSearch access check"]
|
|
33
|
+
H --> D["Exact signed-download origins"]
|
|
29
34
|
M --> Q["Private transaction quarantine"]
|
|
30
35
|
Q --> V["Checked result and file manifest"]
|
|
31
36
|
V --> A["Atomic asset-directory publication"]
|
|
@@ -34,7 +39,14 @@ flowchart LR
|
|
|
34
39
|
R --> P["Per-file provenance and terminal result"]
|
|
35
40
|
```
|
|
36
41
|
|
|
37
|
-
The provider is the only network adapter.
|
|
42
|
+
The provider is the only network adapter. `asset3d enable` invokes its bounded
|
|
43
|
+
`--check-aw-access` mode before publishing project configuration. That mode calls AW
|
|
44
|
+
`HybridSearch` with the documented `X-Sandbox-Key` header and required 3D search
|
|
45
|
+
fields, then emits only `authentication=sandbox-key` plus the canonical origins of
|
|
46
|
+
returned short-lived `res_url` values. Asset IDs, candidates, signed URLs, request
|
|
47
|
+
bodies, and the key are never returned by the access check.
|
|
48
|
+
|
|
49
|
+
Installed download authorities are
|
|
38
50
|
lexically exact `http://host:port` or `https://host:port` values: explicit port,
|
|
39
51
|
no path (including `/`), no query, fragment, userinfo, or wildcard. The canonical
|
|
40
52
|
one-to-eight member set is sorted and encoded as compact JSON in
|
|
@@ -42,33 +54,43 @@ one-to-eight member set is sorted and encoded as compact JSON in
|
|
|
42
54
|
digest. The provider is responsible for enforcing that set on both initial and
|
|
43
55
|
redirect requests.
|
|
44
56
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
only
|
|
49
|
-
|
|
50
|
-
remain failures rather than silently expanding to a full-catalog scan. Without the
|
|
51
|
-
option, the provider retains the managed AW search adapter.
|
|
57
|
+
Normal installation writes the normalized AW service root to `AW_API_BASE_URL` and an
|
|
58
|
+
absolute user credential-file path to `AW_API_CREDENTIAL_FILE`. It never writes
|
|
59
|
+
`AW_API_SANDBOX_KEY` into project MCP configuration. `--catalog-base-url` remains a
|
|
60
|
+
maintainer-only compatibility adapter for the retired directory service; when used,
|
|
61
|
+
its authority must also appear in `--download-origin`.
|
|
52
62
|
|
|
53
63
|
## Installation ownership
|
|
54
64
|
|
|
55
|
-
`asset3d enable`
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
`asset3d enable` uses the documented AW production service when no override is
|
|
66
|
+
present. Service selection precedence is explicit `--base-url`, then
|
|
67
|
+
`FORGEAX_ASSET_LIBRARY_BASE_URL`, then the built-in production service. Therefore a
|
|
68
|
+
test environment is contacted only when the caller supplies its gateway or service
|
|
69
|
+
URL. The first interactive run asks for the AW Sandbox Key with terminal echo
|
|
70
|
+
disabled. Automation can supply `FORGEAX_ASSET3D_AW_SANDBOX_KEY` from a secret store;
|
|
71
|
+
command-line key arguments are deliberately unsupported.
|
|
72
|
+
|
|
73
|
+
The credential is stored outside the project at
|
|
74
|
+
`~/.forgeax/credentials/asset3d-aw.json` by default. The directory is `0700`; the
|
|
75
|
+
file must be regular, owned by the current user, non-symlink, at most 4 KiB, and
|
|
76
|
+
`0600`. The Provider accepts only the versioned AW credential schema. A failed access
|
|
77
|
+
check or downstream installation rolls back a newly written value. An existing valid
|
|
78
|
+
credential makes repeat enablement idempotent. The plugin never treats Anthropic or
|
|
79
|
+
another model-provider credential as an Asset3D key.
|
|
80
|
+
|
|
81
|
+
The command reuses an exact verified cache when present. Otherwise it opens the
|
|
82
|
+
platform archive carried by the installed `@forgeax/game` package and verifies the
|
|
83
|
+
pinned SHA-256 before any provisioning. No Provider download or source-repository
|
|
84
|
+
authentication occurs during `asset3d enable`. Bundle schema, provider commit,
|
|
85
|
+
platform, Python range, and checked result/receipt schema digests are then verified
|
|
86
|
+
before invoking the offline bundle verifier. Provisioning uses system
|
|
66
87
|
Python 3.11 or 3.12 and the Provider's hash-locked `--provision` flow. The published
|
|
67
88
|
cache is keyed by archive digest under `~/.forgeax/providers/asset3d-search/`.
|
|
68
89
|
|
|
69
90
|
The low-level `asset3d install --provider-bundle ...` command remains a maintainer
|
|
70
|
-
surface for
|
|
71
|
-
|
|
91
|
+
surface for bundle production and controlled evidence; users enable the pinned
|
|
92
|
+
archive carried by their installed Game Plugin rather than selecting an arbitrary
|
|
93
|
+
archive.
|
|
72
94
|
|
|
73
95
|
The installer merges, rather than replaces, the complete `.forgeax/mcp.json` object.
|
|
74
96
|
Unrelated top-level keys and servers survive. The owned entries are local stdio only:
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# `@forgeax/game` 0.3.1 release guide
|
|
2
|
+
|
|
3
|
+
Version `0.3.1` keeps the two-command ForgeaX onboarding contract from `0.3.0` and
|
|
4
|
+
replaces the Asset3D default route with the documented AW `HybridSearch` contract.
|
|
5
|
+
Asset3D remains optional and disabled until a user explicitly enables it.
|
|
6
|
+
|
|
7
|
+
## User contract
|
|
8
|
+
|
|
9
|
+
The ordinary setup flow is unchanged:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx -y @forgeax/game install --ide codex,cursor,claude
|
|
13
|
+
cd /path/to/a-genuinely-empty-directory
|
|
14
|
+
npx -y @forgeax/game init
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Inside an initialized project, enable AW Asset3D separately:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx -y @forgeax/game asset3d enable --base-url <aw-gateway-or-service-url>
|
|
21
|
+
npx -y @forgeax/game asset3d doctor --json
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`--base-url` can be replaced by `FORGEAX_ASSET_LIBRARY_BASE_URL`. The first
|
|
25
|
+
interactive enable asks for the AW Sandbox Key with hidden input. CI or another
|
|
26
|
+
non-interactive host must inject `FORGEAX_ASSET3D_AW_SANDBOX_KEY` from its own secret
|
|
27
|
+
store.
|
|
28
|
+
|
|
29
|
+
> [!IMPORTANT]
|
|
30
|
+
> Do not place the key on the command line, in `.forgeax/mcp.json`, in a repository
|
|
31
|
+
> `.env`, or in a release artifact. `ANTHROPIC_API_KEY` is unrelated and is never
|
|
32
|
+
> accepted as an Asset3D credential.
|
|
33
|
+
|
|
34
|
+
## What changed
|
|
35
|
+
|
|
36
|
+
| Area | `0.3.0` | `0.3.1` |
|
|
37
|
+
|:--|:--|:--|
|
|
38
|
+
| Search adapter | Explicit internal directory catalog | AW `HybridSearch` through the packaged Provider |
|
|
39
|
+
| Authentication | Network gate, reported `authentication=none` | Required `X-Sandbox-Key`, validated before success |
|
|
40
|
+
| Service address | Compiled catalog origin | Explicit `--base-url` or environment configuration |
|
|
41
|
+
| Credential storage | None | User-level versioned JSON, directory `0700`, file `0600` |
|
|
42
|
+
| Project MCP config | Catalog origin and download origins | AW service URL, credential-file path, exact download origins |
|
|
43
|
+
| Failure behavior | Failed when an unknown auth contract appeared | Invalid key/network/address fails closed; new credential is rolled back |
|
|
44
|
+
| Provider delivery | Private external release URL | Digest-pinned archive embedded in the npm package |
|
|
45
|
+
|
|
46
|
+
The Provider accepts either an AW gateway root or the complete
|
|
47
|
+
`trpc.oasismetric.omcontentserver.http` service root and appends `HybridSearch`
|
|
48
|
+
exactly once. Its request includes `depot_name`, `asset_type=1`, text content type,
|
|
49
|
+
the query, `similarity_score`, and page size. Short-lived signed `res_url` values stay
|
|
50
|
+
inside the Provider; the access check exposes only their canonical origins for the
|
|
51
|
+
download sandbox.
|
|
52
|
+
|
|
53
|
+
## Security and artifact gates
|
|
54
|
+
|
|
55
|
+
Before publishing, the exact npm tarball and Provider archive must pass all of these
|
|
56
|
+
checks:
|
|
57
|
+
|
|
58
|
+
- [ ] TypeScript typecheck, full Game Plugin test suite, and build.
|
|
59
|
+
- [ ] Provider AW/catalog/security tests and Python compile check.
|
|
60
|
+
- [ ] Two independently built Provider archives are byte-identical.
|
|
61
|
+
- [ ] Provider verifier provisions the archive under its exact SHA-256 cache key.
|
|
62
|
+
- [ ] Real AW access check succeeds without printing the key or a signed URL.
|
|
63
|
+
- [ ] Real search/download and Engine import/readback succeed in a fresh project.
|
|
64
|
+
- [ ] Package artifact gate accepts only the declared files and immutable digests.
|
|
65
|
+
- [ ] Secret scan covers source diff, npm tarball, its embedded workflow TGZ and
|
|
66
|
+
Provider archive, manifests, and file names.
|
|
67
|
+
- [ ] Registry-downloaded npm tarball is byte-identical to the audited candidate.
|
|
68
|
+
|
|
69
|
+
> [!WARNING]
|
|
70
|
+
> A successful search or download is not Preview acceptance. Release evidence must
|
|
71
|
+
> report Provider access, download, Engine import/readback, and visible Play as
|
|
72
|
+
> separate gates. AW responses do not establish license or commercial-use clearance;
|
|
73
|
+
> those remain asset-release requirements.
|
|
74
|
+
|
|
75
|
+
## Provider inclusion
|
|
76
|
+
|
|
77
|
+
The pinned Darwin arm64 Provider identity for this release is:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
provider commit: c181c48fbffc933a7ce9a0836f7878ca5e6d77e1
|
|
81
|
+
archive sha256: c8d2afd7ba68ae4b3540c927a2ff7133a82ea90975d09cab1d10b003fbbdeed3
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The exact archive is embedded beneath `assets/asset3d/provider/` in the npm tarball.
|
|
85
|
+
`asset3d enable` verifies this SHA-256 before provisioning and does not fetch Provider
|
|
86
|
+
code from GitHub, Gongfeng, or another repository. This makes cache-empty enablement
|
|
87
|
+
depend only on npm installation, local Python 3.11/3.12, the explicitly configured AW
|
|
88
|
+
service, and a valid user-supplied AW key.
|
|
89
|
+
|
|
90
|
+
The repository intentionally does not track the generated 17.2 MB Provider binary.
|
|
91
|
+
Release packaging injects the already audited archive through
|
|
92
|
+
`FORGEAX_ASSET3D_PROVIDER_BUNDLE`; the package artifact gate rejects any tarball that
|
|
93
|
+
omits it or contains different bytes.
|
|
94
|
+
|
|
95
|
+
## Maintainer commands
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
bun run typecheck
|
|
99
|
+
bun test test/*.test.ts
|
|
100
|
+
FORGEAX_ASSET3D_PROVIDER_BUNDLE=<audited-provider-archive> bun run build
|
|
101
|
+
FORGEAX_ASSET3D_PROVIDER_BUNDLE=<audited-provider-archive> \
|
|
102
|
+
npm pack --ignore-scripts --json --pack-destination <audited-directory>
|
|
103
|
+
bun scripts/check-package-artifact.ts <audited-directory>/forgeax-game-0.3.1.tgz
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Publishing, npm registry readback, and any merge request are separate irreversible or
|
|
107
|
+
remote-state steps. Record the exact Git heads and artifact digests at each boundary.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# `@forgeax/game` 0.3.2 release guide
|
|
2
|
+
|
|
3
|
+
Version `0.3.2` keeps Asset3D opt-in while making the documented AW production
|
|
4
|
+
service the default selected by `asset3d enable`. Test and pre-release environments
|
|
5
|
+
remain explicit overrides.
|
|
6
|
+
|
|
7
|
+
## User contract
|
|
8
|
+
|
|
9
|
+
The ordinary setup flow is unchanged:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx -y @forgeax/game install --ide codex,cursor,claude
|
|
13
|
+
cd /path/to/a-genuinely-empty-directory
|
|
14
|
+
npx -y @forgeax/game init
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Inside an initialized project, enable production Asset3D without passing an address:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx -y @forgeax/game asset3d enable
|
|
21
|
+
npx -y @forgeax/game asset3d doctor --json
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
> [!IMPORTANT]
|
|
25
|
+
> Asset3D remains disabled until the user runs `asset3d enable`. A no-argument enable
|
|
26
|
+
> selects production; it does not make `install` or `init` contact AW.
|
|
27
|
+
|
|
28
|
+
To select a non-production environment, pass its gateway or complete service URL:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx -y @forgeax/game asset3d enable \
|
|
32
|
+
--base-url <test-gateway-or-service-url>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Environment selection
|
|
36
|
+
|
|
37
|
+
| Priority | Input | Selected environment |
|
|
38
|
+
|--:|:--|:--|
|
|
39
|
+
| 1 | `--base-url` | Explicit caller-selected service, normally test or pre-release |
|
|
40
|
+
| 2 | `FORGEAX_ASSET_LIBRARY_BASE_URL` | Automation or deployment override |
|
|
41
|
+
| 3 | No override | Documented AW production service |
|
|
42
|
+
|
|
43
|
+
The built-in production root is:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
http://lb-pl74wsqg-5wi8ujmy1fq2746r.clb.usw-tencentclb.com:8008/trpc.oasismetric.omcontentserver.http
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Every selection is normalized so `HybridSearch` is appended exactly once. The
|
|
50
|
+
selected endpoint is validated by the packaged Provider before project configuration
|
|
51
|
+
is published.
|
|
52
|
+
|
|
53
|
+
## Authentication and safety
|
|
54
|
+
|
|
55
|
+
The first enable reads an AW Sandbox Key with hidden terminal input. Automation may
|
|
56
|
+
inject `FORGEAX_ASSET3D_AW_SANDBOX_KEY` from a secret store. The plugin never accepts
|
|
57
|
+
`ANTHROPIC_API_KEY` or another model credential as an Asset3D key.
|
|
58
|
+
|
|
59
|
+
The key remains in the private user credential file. Project MCP configuration
|
|
60
|
+
contains only the selected service URL and the credential-file path. Failed access
|
|
61
|
+
validation or installation rolls back a newly written credential and leaves the
|
|
62
|
+
project disabled.
|
|
63
|
+
|
|
64
|
+
## Release gates
|
|
65
|
+
|
|
66
|
+
- [ ] Default resolution selects the exact documented production root.
|
|
67
|
+
- [ ] Explicit CLI URL overrides the environment and production default.
|
|
68
|
+
- [ ] Environment URL overrides the production default when CLI input is absent.
|
|
69
|
+
- [ ] Real production access check succeeds with a valid AW Sandbox Key.
|
|
70
|
+
- [ ] Real production `search_asset` downloads a valid GLB.
|
|
71
|
+
- [ ] Game Plugin transaction validates and commits the asset through Engine.
|
|
72
|
+
- [ ] `doctor` reports the production Provider live in a fresh installed project.
|
|
73
|
+
- [ ] Exact npm tarball and embedded Provider pass the package and secret gates.
|
|
74
|
+
- [ ] Registry-downloaded tarball matches the audited candidate.
|
|
75
|
+
|
|
76
|
+
> [!WARNING]
|
|
77
|
+
> Provider access and Engine import do not prove visible Play acceptance. If a game
|
|
78
|
+
> consumes the imported asset, verify that game separately in its owned Preview and
|
|
79
|
+
> report the Play result as a distinct gate.
|
|
80
|
+
|
|
81
|
+
## Maintainer commands
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
bun run typecheck
|
|
85
|
+
bun test test/*.test.ts
|
|
86
|
+
FORGEAX_ASSET3D_PROVIDER_BUNDLE=<audited-provider-archive> bun run build
|
|
87
|
+
FORGEAX_ASSET3D_PROVIDER_BUNDLE=<audited-provider-archive> \
|
|
88
|
+
npm pack --ignore-scripts --json --pack-destination <audited-directory>
|
|
89
|
+
bun scripts/check-package-artifact.ts <audited-directory>/forgeax-game-0.3.2.tgz
|
|
90
|
+
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"packageManager": "bun@1.3.14",
|
|
3
3
|
"name": "@forgeax/game",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.2",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"description": "@forgeax/game — an MCP/CLI connector for exact released ForgeaX Engine games and Engine-owned Preview.",
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
"assets",
|
|
23
23
|
"docs/asset3d.md",
|
|
24
24
|
"docs/release-0.3.0.md",
|
|
25
|
+
"docs/release-0.3.1.md",
|
|
26
|
+
"docs/release-0.3.2.md",
|
|
25
27
|
"README.md"
|
|
26
28
|
],
|
|
27
29
|
"scripts": {
|