@forgeax/game 0.3.0 → 0.3.1
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
CHANGED
|
@@ -54,12 +54,16 @@ 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
|
-
> npx -y @forgeax/game asset3d enable
|
|
61
|
+
> npx -y @forgeax/game asset3d enable --base-url <aw-gateway-or-service-url>
|
|
62
62
|
> ```
|
|
63
|
+
>
|
|
64
|
+
> The first enable prompts for the AW Sandbox Key with hidden input, validates it
|
|
65
|
+
> through the packaged Provider, and stores it outside the project in a private
|
|
66
|
+
> user credential file.
|
|
63
67
|
|
|
64
68
|
## Agent completion contract
|
|
65
69
|
|
|
@@ -151,29 +155,41 @@ a live unverifiable PID fails closed as `preview_ownership_unverified`.
|
|
|
151
155
|
|
|
152
156
|
## Asset3D installation and transaction
|
|
153
157
|
|
|
154
|
-
Asset3D is an opt-in, project-local MCP integration for
|
|
155
|
-
|
|
158
|
+
Asset3D is an opt-in, project-local MCP integration for AW HybridSearch. Run it only
|
|
159
|
+
after `init`, while connected to the internal network, using the gateway or service
|
|
160
|
+
URL supplied by the asset-platform administrator:
|
|
156
161
|
|
|
157
162
|
```bash
|
|
158
|
-
npx -y @forgeax/game asset3d enable
|
|
163
|
+
npx -y @forgeax/game asset3d enable --base-url <aw-gateway-or-service-url>
|
|
159
164
|
forgeax-game asset3d doctor --json
|
|
160
165
|
```
|
|
161
166
|
|
|
162
|
-
`
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
`--base-url` accepts either the gateway root or the full
|
|
168
|
+
`trpc.oasismetric.omcontentserver.http` service root. It can instead be supplied by
|
|
169
|
+
`FORGEAX_ASSET_LIBRARY_BASE_URL`. On first use, `enable` reads the key with hidden TTY
|
|
170
|
+
input. Non-interactive automation must inject `FORGEAX_ASSET3D_AW_SANDBOX_KEY` from
|
|
171
|
+
its secret store. The key is validated by the Provider against `HybridSearch` before
|
|
172
|
+
the command reports success.
|
|
173
|
+
|
|
174
|
+
The validated key is stored in
|
|
175
|
+
`~/.forgeax/credentials/asset3d-aw.json` as an owned, non-symlink `0600` file. Project
|
|
176
|
+
`.forgeax/mcp.json` contains only the AW service URL and the credential-file path; it
|
|
177
|
+
never contains the key or a signed asset URL. The Provider access check returns only
|
|
178
|
+
the exact download origins required by the sandbox. Failed validation or installation
|
|
179
|
+
rolls back a newly written credential.
|
|
180
|
+
|
|
181
|
+
The npm package carries the digest-pinned Darwin arm64 Provider archive as an inert
|
|
182
|
+
asset. `enable` reuses the exact verified Provider from
|
|
183
|
+
`~/.forgeax/providers/asset3d-search/<sha256>/`, or provisions that packaged archive
|
|
184
|
+
when the cache is absent. It never downloads Provider code from GitHub or another
|
|
185
|
+
repository. Only after Provider verification and its MCP handshake succeed does it merge the `forgeax` and
|
|
168
186
|
`asset3d-search` entries into `.forgeax/mcp.json` and project the approved Asset3D
|
|
169
187
|
workflow into the Agent hosts selected during `install`.
|
|
170
188
|
|
|
171
189
|
> [!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.
|
|
190
|
+
> Asset3D uses the AW-specific `X-Sandbox-Key` contract. It never reads
|
|
191
|
+
> `ANTHROPIC_API_KEY` or another model-provider key. Do not pass keys as command-line
|
|
192
|
+
> arguments, commit the user credential file, or copy a key into project MCP config.
|
|
177
193
|
|
|
178
194
|
The transaction commands are normally driven by the installed Agent workflow:
|
|
179
195
|
|
|
@@ -191,18 +207,20 @@ forgeax-game asset3d uninstall
|
|
|
191
207
|
<details>
|
|
192
208
|
<summary>Maintainer-only local bundle installation</summary>
|
|
193
209
|
|
|
194
|
-
The low-level command remains available for producing and validating platform
|
|
210
|
+
The low-level command remains available for producing and validating platform bundle
|
|
195
211
|
artifacts. It is not the user onboarding path.
|
|
196
212
|
|
|
197
213
|
```bash
|
|
198
214
|
forgeax-game asset3d install \
|
|
199
215
|
--provider-bundle ./asset3d-search-provider-<commit>-<os>-<arch>.tar.gz \
|
|
200
216
|
--sha256 <archive-sha256> \
|
|
201
|
-
--download-origin
|
|
202
|
-
--
|
|
217
|
+
--download-origin https://<approved-download-host>:443 \
|
|
218
|
+
--aw-base-url <aw-gateway-or-service-url> \
|
|
219
|
+
--aw-credential-file /absolute/path/to/private-credential.json
|
|
203
220
|
```
|
|
204
221
|
|
|
205
|
-
|
|
222
|
+
`--catalog-base-url` remains an explicit compatibility-only adapter for the retired
|
|
223
|
+
directory service; normal users should not select it.
|
|
206
224
|
|
|
207
225
|
</details>
|
|
208
226
|
|
|
@@ -227,5 +245,6 @@ and evidence boundaries.
|
|
|
227
245
|
> acceptance gates.
|
|
228
246
|
|
|
229
247
|
See [docs/runtime.md](docs/runtime.md) for the exact artifact and lifecycle contract.
|
|
230
|
-
For the
|
|
231
|
-
|
|
248
|
+
For the current Asset3D authentication update and maintainer publish checklist, see
|
|
249
|
+
[docs/release-0.3.1.md](docs/release-0.3.1.md). The original two-command onboarding
|
|
250
|
+
contract remains documented in [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.1",
|
|
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.1", "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,211 @@ 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
|
+
function normalizeAwServiceRoot(input) {
|
|
5449
|
+
let parsed;
|
|
5415
5450
|
try {
|
|
5416
|
-
|
|
5417
|
-
method: "HEAD",
|
|
5418
|
-
redirect: "manual",
|
|
5419
|
-
signal: controller.signal
|
|
5420
|
-
})).status;
|
|
5451
|
+
parsed = new URL(input);
|
|
5421
5452
|
} catch {
|
|
5422
|
-
throw new Error("
|
|
5423
|
-
}
|
|
5424
|
-
|
|
5453
|
+
throw new Error("asset3d_base_url_invalid: expected an HTTP(S) AW gateway or service URL");
|
|
5454
|
+
}
|
|
5455
|
+
if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
5456
|
+
throw new Error("asset3d_base_url_invalid: credentials, query, and fragment are forbidden");
|
|
5457
|
+
}
|
|
5458
|
+
let path = parsed.pathname.replace(/\/+$/, "");
|
|
5459
|
+
if (path.endsWith(`${AW_SERVICE_PATH}/HybridSearch`))
|
|
5460
|
+
path = path.slice(0, -"/HybridSearch".length);
|
|
5461
|
+
else if (!path.endsWith(AW_SERVICE_PATH))
|
|
5462
|
+
path = `${path}${AW_SERVICE_PATH}`;
|
|
5463
|
+
parsed.pathname = path;
|
|
5464
|
+
return parsed.toString().replace(/\/$/, "");
|
|
5465
|
+
}
|
|
5466
|
+
function resolveAwServiceRoot(explicit) {
|
|
5467
|
+
const configured = explicit || process.env.FORGEAX_ASSET_LIBRARY_BASE_URL;
|
|
5468
|
+
if (!configured) {
|
|
5469
|
+
throw new Error("asset3d_base_url_required: pass --base-url <AW gateway/service URL> or set FORGEAX_ASSET_LIBRARY_BASE_URL");
|
|
5470
|
+
}
|
|
5471
|
+
return normalizeAwServiceRoot(configured);
|
|
5472
|
+
}
|
|
5473
|
+
function checkAwProviderAccess(options) {
|
|
5474
|
+
const command = resolve12(options.providerCache, "bin", "asset3d-search");
|
|
5475
|
+
if (!existsSync10(command))
|
|
5476
|
+
throw new Error("asset3d_provider_not_prepared");
|
|
5477
|
+
const result = spawnSync5(command, ["--check-aw-access"], {
|
|
5478
|
+
encoding: "utf8",
|
|
5479
|
+
timeout: 45000,
|
|
5480
|
+
maxBuffer: 128 * 1024,
|
|
5481
|
+
env: {
|
|
5482
|
+
...process.env,
|
|
5483
|
+
ASSET3D_CATALOG_BASE_URL: "",
|
|
5484
|
+
AW_API_BASE_URL: options.serviceRoot,
|
|
5485
|
+
AW_API_CREDENTIAL_FILE: resolve12(options.credentialFile),
|
|
5486
|
+
AW_API_SANDBOX_KEY: ""
|
|
5487
|
+
}
|
|
5488
|
+
});
|
|
5489
|
+
let payload;
|
|
5490
|
+
try {
|
|
5491
|
+
payload = JSON.parse(result.stdout);
|
|
5492
|
+
} catch {
|
|
5493
|
+
throw new Error("asset3d_access_validation_failed: Provider returned an invalid response");
|
|
5425
5494
|
}
|
|
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");
|
|
5495
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
5496
|
+
throw new Error("asset3d_access_validation_failed: Provider returned an invalid response");
|
|
5430
5497
|
}
|
|
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;
|
|
5498
|
+
const envelope = payload;
|
|
5499
|
+
if (result.status !== 0 || envelope.ok !== true) {
|
|
5500
|
+
const code = envelope.error?.code === "asset3d_access_validation_inconclusive" ? "asset3d_access_validation_inconclusive" : "asset3d_api_key_invalid_or_unavailable";
|
|
5501
|
+
throw new Error(`${code}: verify the AW service address, internal network, and Sandbox Key`);
|
|
5458
5502
|
}
|
|
5459
|
-
if (
|
|
5460
|
-
throw new Error(
|
|
5503
|
+
if (envelope.schemaVersion !== ACCESS_CHECK_SCHEMA || envelope.value?.authentication !== "sandbox-key" || !Array.isArray(envelope.value.downloadOrigins)) {
|
|
5504
|
+
throw new Error("asset3d_access_validation_failed: Provider returned an invalid response");
|
|
5461
5505
|
}
|
|
5462
|
-
const
|
|
5463
|
-
|
|
5464
|
-
|
|
5506
|
+
const origins = canonicalizeOrigins(envelope.value.downloadOrigins).values;
|
|
5507
|
+
return { serviceRoot: options.serviceRoot, authentication: "sandbox-key", downloadOrigins: origins };
|
|
5508
|
+
}
|
|
5509
|
+
|
|
5510
|
+
// src/asset3d/credentials.ts
|
|
5511
|
+
import { chmodSync as chmodSync5, existsSync as existsSync11, lstatSync as lstatSync9, readFileSync as readFileSync14, statSync as statSync7, unlinkSync as unlinkSync4 } from "node:fs";
|
|
5512
|
+
import { homedir as homedir4 } from "node:os";
|
|
5513
|
+
import { dirname as dirname11, isAbsolute as isAbsolute5, resolve as resolve13 } from "node:path";
|
|
5514
|
+
var AW_CREDENTIAL_SCHEMA = "forgeax.asset3d-credential/1.0.0";
|
|
5515
|
+
var AW_KEY_ENV = "FORGEAX_ASSET3D_AW_SANDBOX_KEY";
|
|
5516
|
+
var MAX_CREDENTIAL_BYTES = 4096;
|
|
5517
|
+
function validateKey(value) {
|
|
5518
|
+
if (!value || value.length > 2048 || [...value].some((character) => {
|
|
5519
|
+
const code = character.charCodeAt(0);
|
|
5520
|
+
return code < 32 || code === 127;
|
|
5521
|
+
})) {
|
|
5522
|
+
throw new Error("asset3d_api_key_invalid: expected a non-empty printable key");
|
|
5465
5523
|
}
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5524
|
+
return value;
|
|
5525
|
+
}
|
|
5526
|
+
function defaultAwCredentialFile() {
|
|
5527
|
+
const configured = process.env.FORGEAX_ASSET3D_CREDENTIAL_FILE;
|
|
5528
|
+
return resolve13(configured || resolve13(homedir4(), ".forgeax", "credentials", "asset3d-aw.json"));
|
|
5529
|
+
}
|
|
5530
|
+
function readAwCredential(pathInput) {
|
|
5531
|
+
const path = resolve13(pathInput);
|
|
5532
|
+
if (!isAbsolute5(pathInput))
|
|
5533
|
+
throw new Error("asset3d_credential_path_invalid: absolute path required");
|
|
5534
|
+
if (!existsSync11(path))
|
|
5535
|
+
return;
|
|
5536
|
+
try {
|
|
5537
|
+
const metadata = lstatSync9(path);
|
|
5538
|
+
const wrongOwner = typeof process.getuid === "function" && metadata.uid !== process.getuid();
|
|
5539
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || wrongOwner)
|
|
5540
|
+
throw new Error;
|
|
5541
|
+
if ((metadata.mode & 63) !== 0 || metadata.size < 1 || metadata.size > MAX_CREDENTIAL_BYTES)
|
|
5542
|
+
throw new Error;
|
|
5543
|
+
const parsed = JSON.parse(readFileSync14(path, "utf8"));
|
|
5544
|
+
if (Object.keys(parsed).sort().join(",") !== "provider,sandboxKey,schemaVersion")
|
|
5545
|
+
throw new Error;
|
|
5546
|
+
if (parsed.schemaVersion !== AW_CREDENTIAL_SCHEMA || parsed.provider !== "aw" || typeof parsed.sandboxKey !== "string")
|
|
5547
|
+
throw new Error;
|
|
5548
|
+
return validateKey(parsed.sandboxKey);
|
|
5549
|
+
} catch {
|
|
5550
|
+
throw new Error("asset3d_credential_invalid: credential file must be an owned 0600 regular file with the supported schema");
|
|
5481
5551
|
}
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
if (
|
|
5485
|
-
throw new Error(`
|
|
5552
|
+
}
|
|
5553
|
+
async function promptAwKey() {
|
|
5554
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || typeof process.stdin.setRawMode !== "function") {
|
|
5555
|
+
throw new Error(`asset3d_api_key_required: set ${AW_KEY_ENV} or rerun in an interactive terminal`);
|
|
5486
5556
|
}
|
|
5487
|
-
|
|
5488
|
-
const
|
|
5557
|
+
process.stdout.write("AW Asset3D Sandbox Key (input hidden): ");
|
|
5558
|
+
const input = process.stdin;
|
|
5559
|
+
const previousRaw = input.isRaw;
|
|
5560
|
+
input.setRawMode(true);
|
|
5561
|
+
input.resume();
|
|
5562
|
+
input.setEncoding("utf8");
|
|
5489
5563
|
try {
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5564
|
+
const value = await new Promise((resolveValue, reject) => {
|
|
5565
|
+
let collected = "";
|
|
5566
|
+
const onData = (chunk) => {
|
|
5567
|
+
for (const character of chunk) {
|
|
5568
|
+
if (character === "\x03") {
|
|
5569
|
+
input.off("data", onData);
|
|
5570
|
+
reject(new Error("asset3d_api_key_input_cancelled"));
|
|
5571
|
+
return;
|
|
5572
|
+
}
|
|
5573
|
+
if (character === "\r" || character === `
|
|
5574
|
+
`) {
|
|
5575
|
+
input.off("data", onData);
|
|
5576
|
+
resolveValue(collected);
|
|
5577
|
+
return;
|
|
5578
|
+
}
|
|
5579
|
+
if (character === "" || character === "\b")
|
|
5580
|
+
collected = collected.slice(0, -1);
|
|
5581
|
+
else
|
|
5582
|
+
collected += character;
|
|
5583
|
+
}
|
|
5584
|
+
};
|
|
5585
|
+
input.on("data", onData);
|
|
5586
|
+
});
|
|
5587
|
+
process.stdout.write(`
|
|
5588
|
+
`);
|
|
5589
|
+
return validateKey(value);
|
|
5590
|
+
} finally {
|
|
5591
|
+
input.setRawMode(previousRaw ?? false);
|
|
5592
|
+
input.pause();
|
|
5593
|
+
}
|
|
5594
|
+
}
|
|
5595
|
+
async function acquireAwKey(path) {
|
|
5596
|
+
const fromEnvironment = process.env[AW_KEY_ENV];
|
|
5597
|
+
if (fromEnvironment)
|
|
5598
|
+
return { key: validateKey(fromEnvironment), source: "environment" };
|
|
5599
|
+
const stored = readAwCredential(path);
|
|
5600
|
+
if (stored)
|
|
5601
|
+
return { key: stored, source: "stored" };
|
|
5602
|
+
return { key: await promptAwKey(), source: "prompt" };
|
|
5603
|
+
}
|
|
5604
|
+
function writeAwCredential(pathInput, keyInput) {
|
|
5605
|
+
if (!isAbsolute5(pathInput))
|
|
5606
|
+
throw new Error("asset3d_credential_path_invalid: absolute path required");
|
|
5607
|
+
const path = resolve13(pathInput);
|
|
5608
|
+
const key = validateKey(keyInput);
|
|
5609
|
+
const parent = dirname11(path);
|
|
5610
|
+
ensurePrivateDir(parent);
|
|
5611
|
+
const parentMetadata = lstatSync9(parent);
|
|
5612
|
+
const wrongParentOwner = typeof process.getuid === "function" && parentMetadata.uid !== process.getuid();
|
|
5613
|
+
if (!parentMetadata.isDirectory() || parentMetadata.isSymbolicLink() || wrongParentOwner) {
|
|
5614
|
+
throw new Error("asset3d_credential_path_invalid: parent must be an owned regular directory");
|
|
5615
|
+
}
|
|
5616
|
+
chmodSync5(parent, 448);
|
|
5617
|
+
const existed = existsSync11(path);
|
|
5618
|
+
if (existed)
|
|
5619
|
+
readAwCredential(path);
|
|
5620
|
+
const previous = existed ? readFileSync14(path) : undefined;
|
|
5621
|
+
const previousMode = existed ? statSync7(path).mode & 511 : undefined;
|
|
5622
|
+
const bytes = `${JSON.stringify({ schemaVersion: AW_CREDENTIAL_SCHEMA, provider: "aw", sandboxKey: key }, null, 2)}
|
|
5623
|
+
`;
|
|
5624
|
+
const changed = !previous || !previous.equals(Buffer.from(bytes));
|
|
5625
|
+
if (changed)
|
|
5626
|
+
atomicWrite(path, bytes, 384);
|
|
5627
|
+
chmodSync5(path, 384);
|
|
5628
|
+
let active = true;
|
|
5629
|
+
return {
|
|
5630
|
+
path,
|
|
5631
|
+
changed,
|
|
5632
|
+
commit() {
|
|
5633
|
+
active = false;
|
|
5634
|
+
},
|
|
5635
|
+
rollback() {
|
|
5636
|
+
if (!active || !changed)
|
|
5637
|
+
return;
|
|
5638
|
+
if (previous) {
|
|
5639
|
+
atomicWrite(path, previous, previousMode ?? 384);
|
|
5640
|
+
chmodSync5(path, previousMode ?? 384);
|
|
5641
|
+
} else {
|
|
5642
|
+
unlinkSync4(path);
|
|
5643
|
+
}
|
|
5644
|
+
active = false;
|
|
5645
|
+
}
|
|
5646
|
+
};
|
|
5496
5647
|
}
|
|
5497
5648
|
|
|
5498
5649
|
// src/cli/dispatch.ts
|
|
@@ -5507,8 +5658,8 @@ Usage:
|
|
|
5507
5658
|
forgeax-game preview stop [--game <slug>] [--target-dir <path>] [--json]
|
|
5508
5659
|
forgeax-game devkit install
|
|
5509
5660
|
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> [--
|
|
5661
|
+
forgeax-game asset3d enable [--base-url <AW gateway/service URL>] [--ide ${CLIENT_CHOICES.join(",")}]
|
|
5662
|
+
forgeax-game asset3d install --provider-bundle <archive> --sha256 <hex> --download-origin <scheme://host:port> [--aw-base-url <URL> --aw-credential-file <absolute path>] [...]
|
|
5512
5663
|
forgeax-game asset3d uninstall
|
|
5513
5664
|
forgeax-game asset3d begin --query <text> [--query <text> ...] --json
|
|
5514
5665
|
forgeax-game asset3d commit --execution <uuid> --provider-result-stdin --json [--refresh]
|
|
@@ -5560,23 +5711,23 @@ function requireProject() {
|
|
|
5560
5711
|
return project.root;
|
|
5561
5712
|
}
|
|
5562
5713
|
function updateAgentsFile(root) {
|
|
5563
|
-
const path =
|
|
5564
|
-
const existing =
|
|
5714
|
+
const path = join11(root, "AGENTS.md");
|
|
5715
|
+
const existing = existsSync12(path) ? readFileSync15(path, "utf8") : undefined;
|
|
5565
5716
|
const content = upsertBlock(existing, ROUTING_TEXT);
|
|
5566
5717
|
if (content === existing)
|
|
5567
5718
|
return { path, changed: false };
|
|
5568
|
-
|
|
5719
|
+
writeFileSync11(path, content);
|
|
5569
5720
|
return { path, changed: true };
|
|
5570
5721
|
}
|
|
5571
5722
|
function removeAgentsBlock(root) {
|
|
5572
|
-
const path =
|
|
5573
|
-
if (!
|
|
5723
|
+
const path = join11(root, "AGENTS.md");
|
|
5724
|
+
if (!existsSync12(path))
|
|
5574
5725
|
return { path, changed: false };
|
|
5575
|
-
const existing =
|
|
5726
|
+
const existing = readFileSync15(path, "utf8");
|
|
5576
5727
|
const content = removeBlock(existing);
|
|
5577
5728
|
if (content === existing)
|
|
5578
5729
|
return { path, changed: false };
|
|
5579
|
-
|
|
5730
|
+
writeFileSync11(path, content);
|
|
5580
5731
|
return { path, changed: true };
|
|
5581
5732
|
}
|
|
5582
5733
|
async function installCommand(args) {
|
|
@@ -5675,7 +5826,7 @@ async function useCommand(args) {
|
|
|
5675
5826
|
throw new Error(`game ${JSON.stringify(slug)} not found. Available: ${listGames(root).join(", ") || "(none)"}`);
|
|
5676
5827
|
}
|
|
5677
5828
|
if (listGames(root).length > 1) {
|
|
5678
|
-
|
|
5829
|
+
writeFileSync11(join11(root, ".forgeax", "active-game.json"), `${JSON.stringify({ version: 1, slug }, null, 2)}
|
|
5679
5830
|
`, "utf8");
|
|
5680
5831
|
}
|
|
5681
5832
|
process.stdout.write(`Active game: ${slug}
|
|
@@ -5777,7 +5928,7 @@ async function uninstallCommand(args) {
|
|
|
5777
5928
|
const agents = removeAgentsBlock(root);
|
|
5778
5929
|
process.stdout.write(`${agents.changed ? "REMOVED" : "ABSENT "} routing block: ${agents.path}
|
|
5779
5930
|
`);
|
|
5780
|
-
process.stdout.write(`KEPT your games and project metadata: ${
|
|
5931
|
+
process.stdout.write(`KEPT your games and project metadata: ${join11(root, ".forgeax")}
|
|
5781
5932
|
`);
|
|
5782
5933
|
} else {
|
|
5783
5934
|
process.stdout.write(`INFO no ForgeaX project bound; only client configuration was touched.
|
|
@@ -6007,54 +6158,93 @@ function canonicalAsset3dClients(value) {
|
|
|
6007
6158
|
throw new Error(`asset3d_client_invalid: ${invalid.join(", ")}`);
|
|
6008
6159
|
return [...new Set(values)];
|
|
6009
6160
|
}
|
|
6161
|
+
function parseAsset3dEnableArgs(args) {
|
|
6162
|
+
const ideArgs = [];
|
|
6163
|
+
let baseUrl;
|
|
6164
|
+
for (let index = 0;index < args.length; index++) {
|
|
6165
|
+
const arg = args[index];
|
|
6166
|
+
if (arg === "--base-url") {
|
|
6167
|
+
const value = args[++index];
|
|
6168
|
+
if (!value)
|
|
6169
|
+
throw new Error("--base-url requires an AW gateway/service URL");
|
|
6170
|
+
baseUrl = value;
|
|
6171
|
+
continue;
|
|
6172
|
+
}
|
|
6173
|
+
if (arg.startsWith("--base-url=")) {
|
|
6174
|
+
baseUrl = arg.slice("--base-url=".length);
|
|
6175
|
+
continue;
|
|
6176
|
+
}
|
|
6177
|
+
if (arg === "--ide") {
|
|
6178
|
+
const value = args[++index];
|
|
6179
|
+
if (!value)
|
|
6180
|
+
throw new Error("--ide requires a comma-separated client list");
|
|
6181
|
+
ideArgs.push("--ide", value);
|
|
6182
|
+
continue;
|
|
6183
|
+
}
|
|
6184
|
+
if (arg.startsWith("--ide=")) {
|
|
6185
|
+
ideArgs.push(arg);
|
|
6186
|
+
continue;
|
|
6187
|
+
}
|
|
6188
|
+
throw new Error("usage: forgeax-game asset3d enable [--base-url <AW gateway/service URL>] [--ide codex,claude,cursor,...]");
|
|
6189
|
+
}
|
|
6190
|
+
return { requested: parseIdeSelector(ideArgs, "invalid --ide selector"), baseUrl };
|
|
6191
|
+
}
|
|
6010
6192
|
async function asset3dCommand(args) {
|
|
6011
6193
|
const [operation, ...rest] = args;
|
|
6012
6194
|
const projectRoot = requireProject();
|
|
6013
6195
|
try {
|
|
6014
6196
|
if (operation === "enable") {
|
|
6015
|
-
const
|
|
6016
|
-
const selection = selectClients(projectRoot, requested);
|
|
6197
|
+
const parsed = parseAsset3dEnableArgs(rest);
|
|
6198
|
+
const selection = selectClients(projectRoot, parsed.requested);
|
|
6017
6199
|
reportMissingClients(selection.missing);
|
|
6018
6200
|
if (selection.selected.length === 0) {
|
|
6019
6201
|
throw new Error("asset3d_client_missing: run `forgeax-game install --ide <client>` before enabling Asset3D");
|
|
6020
6202
|
}
|
|
6021
|
-
const
|
|
6203
|
+
const serviceRoot = resolveAwServiceRoot(parsed.baseUrl);
|
|
6022
6204
|
const os = platform2() === "win32" ? "windows" : platform2();
|
|
6023
6205
|
const cpu = arch2();
|
|
6024
6206
|
const target = `${os}-${cpu}`;
|
|
6025
|
-
const
|
|
6026
|
-
if (!
|
|
6207
|
+
const bundled = BUNDLED_ASSET3D_PROVIDERS[target];
|
|
6208
|
+
if (!bundled)
|
|
6027
6209
|
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
|
-
}
|
|
6210
|
+
const providerCache = resolve14(homedir5(), ".forgeax", "providers", "asset3d-search", bundled.sha256);
|
|
6211
|
+
let preparedCache = providerCache;
|
|
6212
|
+
if (!existsSync12(providerCache)) {
|
|
6213
|
+
const providerBundle = packagedAsset3dProvider(bundled.relativePath);
|
|
6214
|
+
preparedCache = prepareAsset3dProvider({ providerBundle, expectedSha256: bundled.sha256 });
|
|
6049
6215
|
}
|
|
6050
|
-
|
|
6216
|
+
const credentialFile = defaultAwCredentialFile();
|
|
6217
|
+
const credential = await acquireAwKey(credentialFile);
|
|
6218
|
+
const credentialWrite = writeAwCredential(credentialFile, credential.key);
|
|
6219
|
+
const clients = selection.selected.filter((id) => (id in SKILL_MOUNTS));
|
|
6220
|
+
try {
|
|
6221
|
+
const access = checkAwProviderAccess({ providerCache: preparedCache, serviceRoot, credentialFile });
|
|
6222
|
+
const result = await installProvisionedAsset3d({
|
|
6223
|
+
projectRoot,
|
|
6224
|
+
providerCache: preparedCache,
|
|
6225
|
+
expectedSha256: bundled.sha256,
|
|
6226
|
+
downloadOrigins: access.downloadOrigins,
|
|
6227
|
+
awApiBaseUrl: access.serviceRoot,
|
|
6228
|
+
awCredentialFile: credentialFile,
|
|
6229
|
+
clients,
|
|
6230
|
+
gamePluginLaunch: launchSpec("npx"),
|
|
6231
|
+
replaceOwned: true
|
|
6232
|
+
});
|
|
6233
|
+
credentialWrite.commit();
|
|
6234
|
+
process.stdout.write(`${result.changed ? "ENABLED" : "CURRENT"} Asset3D provider ${result.providerCommit}; service=${access.serviceRoot}; authentication=${access.authentication}; skillFiles=${result.skillFiles}
|
|
6051
6235
|
`);
|
|
6052
|
-
|
|
6236
|
+
return 0;
|
|
6237
|
+
} catch (error) {
|
|
6238
|
+
credentialWrite.rollback();
|
|
6239
|
+
throw error;
|
|
6240
|
+
}
|
|
6053
6241
|
}
|
|
6054
6242
|
if (operation === "install") {
|
|
6055
6243
|
let providerBundle;
|
|
6056
6244
|
let expectedSha256;
|
|
6057
6245
|
let catalogBaseUrl;
|
|
6246
|
+
let awApiBaseUrl;
|
|
6247
|
+
let awCredentialFile;
|
|
6058
6248
|
const downloadOrigins = [];
|
|
6059
6249
|
let clients;
|
|
6060
6250
|
let replaceOwned = false;
|
|
@@ -6064,7 +6254,7 @@ async function asset3dCommand(args) {
|
|
|
6064
6254
|
replaceOwned = true;
|
|
6065
6255
|
continue;
|
|
6066
6256
|
}
|
|
6067
|
-
if (arg === "--provider-bundle" || arg === "--sha256" || arg === "--download-origin" || arg === "--catalog-base-url" || arg === "--ide") {
|
|
6257
|
+
if (arg === "--provider-bundle" || arg === "--sha256" || arg === "--download-origin" || arg === "--catalog-base-url" || arg === "--aw-base-url" || arg === "--aw-credential-file" || arg === "--ide") {
|
|
6068
6258
|
const value = rest[++index];
|
|
6069
6259
|
if (!value)
|
|
6070
6260
|
throw new Error(`${arg} requires a value`);
|
|
@@ -6076,6 +6266,10 @@ async function asset3dCommand(args) {
|
|
|
6076
6266
|
downloadOrigins.push(value);
|
|
6077
6267
|
else if (arg === "--catalog-base-url")
|
|
6078
6268
|
catalogBaseUrl = value;
|
|
6269
|
+
else if (arg === "--aw-base-url")
|
|
6270
|
+
awApiBaseUrl = resolveAwServiceRoot(value);
|
|
6271
|
+
else if (arg === "--aw-credential-file")
|
|
6272
|
+
awCredentialFile = resolve14(value);
|
|
6079
6273
|
else
|
|
6080
6274
|
clients = canonicalAsset3dClients(value);
|
|
6081
6275
|
continue;
|
|
@@ -6084,7 +6278,17 @@ async function asset3dCommand(args) {
|
|
|
6084
6278
|
}
|
|
6085
6279
|
if (!providerBundle || !expectedSha256)
|
|
6086
6280
|
throw new Error("usage: forgeax-game asset3d install --provider-bundle <archive> --sha256 <hex> --download-origin <origin> [...]");
|
|
6087
|
-
const result = await installAsset3d({
|
|
6281
|
+
const result = await installAsset3d({
|
|
6282
|
+
projectRoot,
|
|
6283
|
+
providerBundle,
|
|
6284
|
+
expectedSha256,
|
|
6285
|
+
downloadOrigins,
|
|
6286
|
+
...catalogBaseUrl ? { catalogBaseUrl } : {},
|
|
6287
|
+
...awApiBaseUrl ? { awApiBaseUrl } : {},
|
|
6288
|
+
...awCredentialFile ? { awCredentialFile } : {},
|
|
6289
|
+
...clients ? { clients } : {},
|
|
6290
|
+
replaceOwned
|
|
6291
|
+
});
|
|
6088
6292
|
process.stdout.write(`${result.changed ? "INSTALLED" : "CURRENT"} Asset3D provider ${result.providerCommit}; originSetDigest=${result.originSetDigest}; skillFiles=${result.skillFiles}
|
|
6089
6293
|
`);
|
|
6090
6294
|
return 0;
|
|
@@ -6259,7 +6463,7 @@ function parseMcpArgs(args) {
|
|
|
6259
6463
|
} else
|
|
6260
6464
|
throw new Error(`unknown MCP option: ${arg}`);
|
|
6261
6465
|
}
|
|
6262
|
-
return { transport, host, port, root:
|
|
6466
|
+
return { transport, host, port, root: resolve15(root), requireAuth, allowedOrigins };
|
|
6263
6467
|
}
|
|
6264
6468
|
async function runMcp(args) {
|
|
6265
6469
|
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,41 @@ 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
|
-
The
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
`asset3d enable` requires an AW gateway or full service URL through `--base-url` or
|
|
66
|
+
`FORGEAX_ASSET_LIBRARY_BASE_URL`. No private-network endpoint is compiled into the
|
|
67
|
+
package. The first interactive run asks for the AW Sandbox Key with terminal echo
|
|
68
|
+
disabled. Automation can supply `FORGEAX_ASSET3D_AW_SANDBOX_KEY` from a secret store;
|
|
69
|
+
command-line key arguments are deliberately unsupported.
|
|
70
|
+
|
|
71
|
+
The credential is stored outside the project at
|
|
72
|
+
`~/.forgeax/credentials/asset3d-aw.json` by default. The directory is `0700`; the
|
|
73
|
+
file must be regular, owned by the current user, non-symlink, at most 4 KiB, and
|
|
74
|
+
`0600`. The Provider accepts only the versioned AW credential schema. A failed access
|
|
75
|
+
check or downstream installation rolls back a newly written value. An existing valid
|
|
76
|
+
credential makes repeat enablement idempotent. The plugin never treats Anthropic or
|
|
77
|
+
another model-provider credential as an Asset3D key.
|
|
78
|
+
|
|
79
|
+
The command reuses an exact verified cache when present. Otherwise it opens the
|
|
80
|
+
platform archive carried by the installed `@forgeax/game` package and verifies the
|
|
81
|
+
pinned SHA-256 before any provisioning. No Provider download or source-repository
|
|
82
|
+
authentication occurs during `asset3d enable`. Bundle schema, provider commit,
|
|
83
|
+
platform, Python range, and checked result/receipt schema digests are then verified
|
|
84
|
+
before invoking the offline bundle verifier. Provisioning uses system
|
|
66
85
|
Python 3.11 or 3.12 and the Provider's hash-locked `--provision` flow. The published
|
|
67
86
|
cache is keyed by archive digest under `~/.forgeax/providers/asset3d-search/`.
|
|
68
87
|
|
|
69
88
|
The low-level `asset3d install --provider-bundle ...` command remains a maintainer
|
|
70
|
-
surface for
|
|
71
|
-
|
|
89
|
+
surface for bundle production and controlled evidence; users enable the pinned
|
|
90
|
+
archive carried by their installed Game Plugin rather than selecting an arbitrary
|
|
91
|
+
archive.
|
|
72
92
|
|
|
73
93
|
The installer merges, rather than replaces, the complete `.forgeax/mcp.json` object.
|
|
74
94
|
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.
|
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.1",
|
|
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,7 @@
|
|
|
22
22
|
"assets",
|
|
23
23
|
"docs/asset3d.md",
|
|
24
24
|
"docs/release-0.3.0.md",
|
|
25
|
+
"docs/release-0.3.1.md",
|
|
25
26
|
"README.md"
|
|
26
27
|
],
|
|
27
28
|
"scripts": {
|