@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, download a platform Provider, or add
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 the internal catalog. Run it
155
- only after `init` and while connected to the internal network:
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
- `enable` first probes `http://21.214.216.8:5180/api/assets` with a body-free `HEAD`
163
- request. The current network-gated service returns `200` and requires no API Key, so
164
- the plugin reports `authentication=none`. It then reuses the exact verified Provider
165
- from `~/.forgeax/providers/asset3d-search/<sha256>/`, or downloads the digest-pinned
166
- platform release artifact over HTTPS when the cache is absent. Only after Provider
167
- verification and its MCP handshake succeed does the command merge the `forgeax` and
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
- > The internal catalog does **not** use `ANTHROPIC_API_KEY`. No model-provider key is
173
- > accepted, copied, or written by `asset3d enable`. If a future catalog returns
174
- > `401`/`403`, this release fails closed with `asset3d_api_key_contract_missing` until
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 release
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 http://21.214.216.8:5180 \
202
- --catalog-base-url http://21.214.216.8:5180
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
- The catalog authority must also be an exact member of `--download-origin`.
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 `0.3.0` onboarding release, migration, verification, and maintainer publish
231
- checklist, see [docs/release-0.3.0.md](docs/release-0.3.0.md).
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).
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 resolve13 } from "node:path";
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.0",
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 existsSync10, readFileSync as readFileSync14, writeFileSync as writeFileSync12 } from "node:fs";
3230
- import { arch as arch2, homedir as homedir4, platform as platform2 } from "node:os";
3231
- import { join as join12, resolve as resolve12 } from "node:path";
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.0", "forgeax-game", "mcp"]
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 = "68749b1f36164a1e2ea8c67110676614bb9a013e";
4190
- var DEFAULT_ASSET3D_CATALOG_ORIGIN = "http://21.214.216.8:5180";
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: "00ea7c977a705f2a9807c90d864c021c54c174d7f8295c2d263022d968480357",
4194
- url: "https://github.com/ForgeaXGame/forgeax-game-plugin/releases/download/asset3d-provider-68749b1f36164a1e2ea8c67110676614bb9a013e/asset3d-search-provider-68749b1f36164a1e2ea8c67110676614bb9a013e-darwin-arm64.tar.gz"
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/catalog-access.ts
5410
- async function probeAsset3dCatalog(inputOrigin, request2 = fetch) {
5411
- const origin = canonicalizeOrigins([inputOrigin]).values[0];
5412
- const controller = new AbortController;
5413
- const timeout = setTimeout(() => controller.abort(), 1e4);
5414
- let status;
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
- status = (await request2(`${origin}/api/assets`, {
5417
- method: "HEAD",
5418
- redirect: "manual",
5419
- signal: controller.signal
5420
- })).status;
5451
+ parsed = new URL(input);
5421
5452
  } catch {
5422
- throw new Error("asset3d_catalog_unreachable: connect to the internal network and retry");
5423
- } finally {
5424
- clearTimeout(timeout);
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 (status >= 200 && status < 300)
5427
- return { origin, authentication: "none" };
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
- throw new Error(`asset3d_catalog_probe_failed: HEAD /api/assets returned ${status}`);
5432
- }
5433
-
5434
- // src/asset3d/release.ts
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 (!response || response.status !== 200 || !response.body) {
5460
- throw new Error(`asset3d_provider_download_failed: release artifact returned ${response?.status ?? "too_many_redirects"}`);
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 declared = Number(response.headers.get("content-length") ?? "0");
5463
- if (Number.isFinite(declared) && declared > MAX_PROVIDER_ARCHIVE_BYTES) {
5464
- throw new Error("asset3d_provider_download_too_large");
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
- const chunks = [];
5467
- let total = 0;
5468
- const reader = response.body.getReader();
5469
- for (;; ) {
5470
- const { done, value } = await reader.read();
5471
- if (done)
5472
- break;
5473
- if (!value)
5474
- continue;
5475
- total += value.byteLength;
5476
- if (total > MAX_PROVIDER_ARCHIVE_BYTES) {
5477
- await reader.cancel();
5478
- throw new Error("asset3d_provider_download_too_large");
5479
- }
5480
- chunks.push(value);
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
- const bytes = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), total);
5483
- const actual = createHash8("sha256").update(bytes).digest("hex");
5484
- if (actual !== release.sha256) {
5485
- throw new Error(`provider_bundle_digest_mismatch: expected ${release.sha256}, got ${actual}`);
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
- const root = mkdtempSync3(join11(tmpdir3(), "forgeax-asset3d-release-"));
5488
- const archive = join11(root, basename4(url.pathname) || "provider.tar.gz");
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
- writeFileSync11(archive, bytes, { mode: 384, flag: "wx" });
5491
- } catch (error) {
5492
- rmSync7(root, { recursive: true, force: true });
5493
- throw error;
5494
- }
5495
- return { archive, cleanup: () => rmSync7(root, { recursive: true, force: true }) };
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> [--catalog-base-url <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 = join12(root, "AGENTS.md");
5564
- const existing = existsSync10(path) ? readFileSync14(path, "utf8") : undefined;
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
- writeFileSync12(path, content);
5719
+ writeFileSync11(path, content);
5569
5720
  return { path, changed: true };
5570
5721
  }
5571
5722
  function removeAgentsBlock(root) {
5572
- const path = join12(root, "AGENTS.md");
5573
- if (!existsSync10(path))
5723
+ const path = join11(root, "AGENTS.md");
5724
+ if (!existsSync12(path))
5574
5725
  return { path, changed: false };
5575
- const existing = readFileSync14(path, "utf8");
5726
+ const existing = readFileSync15(path, "utf8");
5576
5727
  const content = removeBlock(existing);
5577
5728
  if (content === existing)
5578
5729
  return { path, changed: false };
5579
- writeFileSync12(path, content);
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
- writeFileSync12(join12(root, ".forgeax", "active-game.json"), `${JSON.stringify({ version: 1, slug }, null, 2)}
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: ${join12(root, ".forgeax")}
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 requested = parseIdeSelector(rest, "usage: forgeax-game asset3d enable [--ide codex,claude,cursor,...]");
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 access = await probeAsset3dCatalog(DEFAULT_ASSET3D_CATALOG_ORIGIN);
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 release = DEFAULT_ASSET3D_PROVIDER_RELEASES[target];
6026
- if (!release)
6207
+ const bundled = BUNDLED_ASSET3D_PROVIDERS[target];
6208
+ if (!bundled)
6027
6209
  throw new Error(`asset3d_provider_target_unreleased: ${target}`);
6028
- const providerCache = resolve12(homedir4(), ".forgeax", "providers", "asset3d-search", release.sha256);
6029
- const clients = selection.selected.filter((id) => (id in SKILL_MOUNTS));
6030
- const common = {
6031
- projectRoot,
6032
- expectedSha256: release.sha256,
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
- process.stdout.write(`${result.changed ? "ENABLED" : "CURRENT"} Asset3D provider ${result.providerCommit}; catalog=${access.origin}; authentication=${access.authentication}; skillFiles=${result.skillFiles}
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
- return 0;
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({ projectRoot, providerBundle, expectedSha256, downloadOrigins, ...catalogBaseUrl ? { catalogBaseUrl } : {}, ...clients ? { clients } : {}, replaceOwned });
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: resolve13(root), requireAuth, allowedOrigins };
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 release artifacts and are not embedded in
17
- > the platform-neutral npm package. Installation deliberately returns
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 published target therefore needs its own digest, offline verification, MCP
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
- B["Digest-pinned local provider bundle"] --> C["Managed digest cache"]
27
- K["Exact K0 workflow TGZ"] --> S["Nine project Skill mounts"]
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. Installed download authorities are
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
- When installation supplies `--catalog-base-url`, the exact pathless authority is
46
- written only to the project-local provider environment. It must also appear in
47
- `--download-origin`. The provider first attempts the bounded `/api/search` route;
48
- only an explicit `404` or `405` permits a bounded `/api/assets` fallback, and the
49
- selected asset is fetched through `/api/download/assets`. Other upstream failures
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` probes the default internal catalog with `HEAD /api/assets`. A `2xx`
56
- response selects `authentication=none`; the current service follows this path and no
57
- API Key is required. `401` or `403` without an explicit supported authentication
58
- contract fails closed as `asset3d_api_key_contract_missing`. The plugin never treats
59
- an Anthropic or other model-provider credential as an Asset3D credential.
60
-
61
- The command reuses an exact verified cache when present. Otherwise it downloads the
62
- platform release archive over HTTPS, follows at most five HTTPS redirects, enforces a
63
- 256 MiB bound, and verifies the pinned SHA-256 before any provisioning. Bundle schema,
64
- provider commit, platform, Python range, and checked result/receipt schema digests are
65
- then verified before invoking the offline bundle verifier. Provisioning uses system
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 release production and controlled evidence; users enable the pinned
71
- release rather than selecting an arbitrary archive.
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.0",
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": {