From 396a9d25f6dcfb8f2c236aa58e4963953aec7af2 Mon Sep 17 00:00:00 2001 From: npc0-hue Date: Thu, 13 Aug 2026 10:47:42 +0800 Subject: [PATCH] Gate SCUM typed RCON catalog declarations --- .../tasks.md | 10 +++++++- .../game-plugin.manifest.schema.json | 2 +- plugins/schemas/scum-live-data.schema.json | 2 +- plugins/scripts/validate-manifest.ts | 3 +-- plugins/sdk/index.ts | 2 +- plugins/tests/manifest-validation.test.ts | 25 +++++++++++++++++++ 6 files changed, 38 insertions(+), 6 deletions(-) diff --git a/openspec/changes/replace-scum-projections-with-real-data-management/tasks.md b/openspec/changes/replace-scum-projections-with-real-data-management/tasks.md index 8a794c6..b417be0 100644 --- a/openspec/changes/replace-scum-projections-with-real-data-management/tasks.md +++ b/openspec/changes/replace-scum-projections-with-real-data-management/tasks.md @@ -62,6 +62,14 @@ - Added manifest-validator checks and fixture tests for map metadata/schema compatibility, transform adapter/schema fingerprint matching, declared bounds/image consistency, known-point projection fixtures, non-finite/out-of-bounds rejection, and digest-preserving adapter incompatibility. - Verification passed: `(cd plugins && npm test -- manifest-validation.test.ts)` and `(cd plugins && npm run validate:manifest)`. +### Typed RCON and gift catalog gate evidence (2026-08-13) + +- Reviewed current-service evidence in `evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md`: Fame/currency commands, notification-as-RCON semantics, gift aliases/transports, conclusive per-item receipts, and catalog aliases remain unverified. Therefore the production SCUM manifest continues to declare no `scumLiveData.typedRconTemplates`, no `scumLiveData.giftCatalogs`, and no digest-referenced RCON/gift assets. +- Kept `economy-command.write` and `gift-command.write` disabled with `missing` evidence status; no command text, gift alias, starter-pack catalog, legacy hard-coded gift, or notification transport is exposed through live-data assets. +- Tightened SDK/schema/validator contracts so any future SCUM typed RCON template must carry a bounded `confirmationSchemaRef` in addition to payload/result schemas, contained asset paths, immutable digests, protected RCON transport, and `server.game-client.command` permission. +- Added manifest tests proving production omits unverified typed RCON templates and gift catalogs, and rejects any SCUM typed RCON declaration without a conclusive confirmation schema. +- Verification passed: `(cd plugins && npm test -- manifest-validation.test.ts)`. + - [x] 3.1 Add SDK and manifest types for versioned log parsers, SQLite query assets, parameter/result schemas, capability-specific schema fingerprints, sync cadence/limits, map metadata, typed RCON templates, gift item catalogs, and guarded mutation declarations. - [x] 3.2 Extend plugin validation to require asset digests, contained package paths, unique template keys, bounded parameters/results, compatible adapter versions, and explicit permission bindings, and to reject raw caller-supplied SQL, RCON, XML, paths, or undeclared parameters. - [x] 3.3 Capture sanitized authentic login-log fixtures from the active service and bind their expected events to server, Run binding, plugin version, parser version/digest, a transport cursor `(source identity, stream generation, sequence)`, and a separate privacy-safe logical event identity stable across rotation overlap. @@ -70,7 +78,7 @@ - [x] 3.6 Add parameterized squad/member, vehicle, flag/territory, and position query assets and exact result schemas, keeping ambiguous ranks, ownership, coordinates, and missing numeric values null. - [x] 3.7 Add query-asset tests for single SELECT/CTE or approved introspection boundaries, parameter binding, pagination/cursors, timeout/row/byte limits, schema-version matching, and rejection of DDL, mutation, `ATTACH`, extension loading, write PRAGMAs, and multi-statement input. - [x] 3.8 Package the authorized SCUM map asset, identity/version, verified world bounds, layer metadata, and coordinate transform, with fixture tests for known points, out-of-bounds/non-finite coordinates, and adapter incompatibility. -- [ ] 3.9 Declare only verified typed RCON templates for supported Fame/currency/notification/gift operations and a version-scoped gift item catalog; omit any command whose execution and confirmation semantics remain unknown. +- [x] 3.9 Declare only verified typed RCON templates for supported Fame/currency/notification/gift operations and a version-scoped gift item catalog; omit any command whose execution and confirmation semantics remain unknown. - [ ] 3.10 Declare a guarded preserving XML mutation only after the real XML source and named attributes are proven; expose `855` only as a reviewed named-attribute preset and never as a database column, generic integer field, or guessed mapping. - [x] 3.11 Add immutable asset/digest declarations and plugin package validation; defer generated Run-package execution wiring until the complete protocol/result envelope and independent Run capability evidence in group 4 are frozen. - [x] 3.12 Remove SCUM Workflow/projection declarations and obsolete page/action declarations from the plugin manifest while preserving the five required pages and AI configuration assistance. diff --git a/plugins/manifests/game-plugin.manifest.schema.json b/plugins/manifests/game-plugin.manifest.schema.json index 2834f00..06dce46 100644 --- a/plugins/manifests/game-plugin.manifest.schema.json +++ b/plugins/manifests/game-plugin.manifest.schema.json @@ -370,7 +370,7 @@ "properties": { "capability": { "$ref": "#/$defs/scumReadCapability" }, "intervalSeconds": { "type": "integer", "minimum": 5, "maximum": 86400 }, "jitterPercent": { "type": "integer", "minimum": 0, "maximum": 100 }, "timeoutMs": { "type": "integer", "minimum": 1, "maximum": 60000 }, "maxConcurrentPerServer": { "type": "integer", "minimum": 1, "maximum": 16 } } }, "scumTypedRconTemplate": { - "allOf": [{ "$ref": "#/$defs/scumVersionedAssetBase" }, { "type": "object", "required": ["capability", "transportKey", "targetKey", "permission", "payloadSchemaRef", "resultSchemaRef", "timeoutMs", "maxPayloadBytes"], "additionalProperties": false, "properties": { + "allOf": [{ "$ref": "#/$defs/scumVersionedAssetBase" }, { "type": "object", "required": ["capability", "transportKey", "targetKey", "permission", "payloadSchemaRef", "resultSchemaRef", "confirmationSchemaRef", "timeoutMs", "maxPayloadBytes"], "additionalProperties": false, "properties": { "key": { "$ref": "#/$defs/logicalKey" }, "adapterVersion": { "$ref": "#/$defs/scumAdapterVersion" }, "assetPath": { "$ref": "#/$defs/relativePathRef" }, "digest": { "$ref": "#/$defs/scumAssetDigest" }, "capability": { "enum": ["economy-command.write", "gift-command.write"] }, "requiredSchemaFingerprint": { "$ref": "#/$defs/scumSchemaFingerprint" }, "transportKey": { "$ref": "#/$defs/logicalKey" }, "targetKey": { "$ref": "#/$defs/logicalKey" }, "permission": { "const": "server.game-client.command" }, "payloadSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "resultSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "confirmationSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "timeoutMs": { "type": "integer", "minimum": 1, "maximum": 60000 }, "maxPayloadBytes": { "type": "integer", "minimum": 1, "maximum": 65536 } } }] }, diff --git a/plugins/schemas/scum-live-data.schema.json b/plugins/schemas/scum-live-data.schema.json index 177ee94..5675f29 100644 --- a/plugins/schemas/scum-live-data.schema.json +++ b/plugins/schemas/scum-live-data.schema.json @@ -72,7 +72,7 @@ "properties": { "capability": { "$ref": "#/$defs/readCapability" }, "intervalSeconds": { "type": "integer", "minimum": 5, "maximum": 86400 }, "jitterPercent": { "type": "integer", "minimum": 0, "maximum": 100 }, "timeoutMs": { "type": "integer", "minimum": 1, "maximum": 60000 }, "maxConcurrentPerServer": { "type": "integer", "minimum": 1, "maximum": 16 } } }, "typedRconTemplate": { - "allOf": [{ "$ref": "#/$defs/assetBase" }, { "type": "object", "required": ["capability", "transportKey", "targetKey", "permission", "payloadSchemaRef", "resultSchemaRef", "timeoutMs", "maxPayloadBytes"], "additionalProperties": false, "properties": { "key": { "$ref": "#/$defs/logicalKey" }, "adapterVersion": { "$ref": "#/$defs/adapterVersion" }, "assetPath": { "$ref": "#/$defs/relativePathRef" }, "digest": { "$ref": "#/$defs/assetDigest" }, "capability": { "enum": ["economy-command.write", "gift-command.write"] }, "requiredSchemaFingerprint": { "$ref": "#/$defs/schemaFingerprint" }, "transportKey": { "$ref": "#/$defs/logicalKey" }, "targetKey": { "$ref": "#/$defs/logicalKey" }, "permission": { "const": "server.game-client.command" }, "payloadSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "resultSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "confirmationSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "timeoutMs": { "type": "integer", "minimum": 1, "maximum": 60000 }, "maxPayloadBytes": { "type": "integer", "minimum": 1, "maximum": 65536 } } }] + "allOf": [{ "$ref": "#/$defs/assetBase" }, { "type": "object", "required": ["capability", "transportKey", "targetKey", "permission", "payloadSchemaRef", "resultSchemaRef", "confirmationSchemaRef", "timeoutMs", "maxPayloadBytes"], "additionalProperties": false, "properties": { "key": { "$ref": "#/$defs/logicalKey" }, "adapterVersion": { "$ref": "#/$defs/adapterVersion" }, "assetPath": { "$ref": "#/$defs/relativePathRef" }, "digest": { "$ref": "#/$defs/assetDigest" }, "capability": { "enum": ["economy-command.write", "gift-command.write"] }, "requiredSchemaFingerprint": { "$ref": "#/$defs/schemaFingerprint" }, "transportKey": { "$ref": "#/$defs/logicalKey" }, "targetKey": { "$ref": "#/$defs/logicalKey" }, "permission": { "const": "server.game-client.command" }, "payloadSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "resultSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "confirmationSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "timeoutMs": { "type": "integer", "minimum": 1, "maximum": 60000 }, "maxPayloadBytes": { "type": "integer", "minimum": 1, "maximum": 65536 } } }] }, "guardedMutation": { "allOf": [{ "$ref": "#/$defs/assetBase" }, { "type": "object", "required": ["capability", "requiredSchemaFingerprint", "transportKey", "targetKey", "permission", "payloadSchemaRef", "resultSchemaRef", "confirmationSchemaRef", "timeoutMs", "maxPayloadBytes", "maxRowsAffected", "safety"], "additionalProperties": false, "properties": { "key": { "$ref": "#/$defs/logicalKey" }, "adapterVersion": { "$ref": "#/$defs/adapterVersion" }, "assetPath": { "$ref": "#/$defs/relativePathRef" }, "digest": { "$ref": "#/$defs/assetDigest" }, "capability": { "const": "profile-xml.write" }, "requiredSchemaFingerprint": { "$ref": "#/$defs/schemaFingerprint" }, "transportKey": { "$ref": "#/$defs/logicalKey" }, "targetKey": { "$ref": "#/$defs/logicalKey" }, "permission": { "const": "server.game-client.maintenance" }, "payloadSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "resultSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "confirmationSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "timeoutMs": { "type": "integer", "minimum": 1, "maximum": 60000 }, "maxPayloadBytes": { "type": "integer", "minimum": 1, "maximum": 65536 }, "maxRowsAffected": { "const": 1 }, "safety": { "type": "object", "required": ["requiresExpectedChecksum", "requiresBackupEvidence", "requiresOfflineOrMaintenance", "requiresReadAfterWrite"], "additionalProperties": false, "properties": { "requiresExpectedChecksum": { "const": true }, "requiresBackupEvidence": { "const": true }, "requiresOfflineOrMaintenance": { "const": true }, "requiresReadAfterWrite": { "const": true } } } } }] diff --git a/plugins/scripts/validate-manifest.ts b/plugins/scripts/validate-manifest.ts index 2192598..d0cbed0 100644 --- a/plugins/scripts/validate-manifest.ts +++ b/plugins/scripts/validate-manifest.ts @@ -1289,8 +1289,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st if (!["economy-command.write", "gift-command.write"].includes(template.capability ?? "")) errors.push(`${itemLocation}.capability: must be economy-command.write or gift-command.write`); if (template.requiredSchemaFingerprint && !fingerprintPattern.test(template.requiredSchemaFingerprint)) errors.push(`${itemLocation}.requiredSchemaFingerprint: must be a schema fingerprint`); if (template.permission !== "server.game-client.command" || !declaredPermissions.has("server.game-client.command")) errors.push(`${itemLocation}.permission: must require declared server.game-client.command`); - requireRefs(template as Record, itemLocation, ["payloadSchemaRef", "resultSchemaRef"]); - if (template.confirmationSchemaRef && !isSafeRelativeJsonRef(template.confirmationSchemaRef)) errors.push(`${itemLocation}.confirmationSchemaRef: must be a safe relative JSON schema reference`); + requireRefs(template as Record, itemLocation, ["payloadSchemaRef", "resultSchemaRef", "confirmationSchemaRef"]); if (!Number.isInteger(template.timeoutMs) || (template.timeoutMs ?? 0) < 1 || (template.timeoutMs ?? 0) > 60000) errors.push(`${itemLocation}.timeoutMs: must be between 1 and 60000`); if (!Number.isInteger(template.maxPayloadBytes) || (template.maxPayloadBytes ?? 0) < 1 || (template.maxPayloadBytes ?? 0) > 65536) errors.push(`${itemLocation}.maxPayloadBytes: must be between 1 and 65536`); const rconTransport = transportByKey(template.transportKey); diff --git a/plugins/sdk/index.ts b/plugins/sdk/index.ts index d3fb184..5037e76 100644 --- a/plugins/sdk/index.ts +++ b/plugins/sdk/index.ts @@ -361,7 +361,7 @@ export interface SCUMTypedRCONTemplateDeclaration extends SCUMVersionedAssetDecl permission: Extract; payloadSchemaRef: string; resultSchemaRef: string; - confirmationSchemaRef?: string; + confirmationSchemaRef: string; timeoutMs: number; maxPayloadBytes: number; } diff --git a/plugins/tests/manifest-validation.test.ts b/plugins/tests/manifest-validation.test.ts index 8555e98..bac9dde 100644 --- a/plugins/tests/manifest-validation.test.ts +++ b/plugins/tests/manifest-validation.test.ts @@ -305,6 +305,19 @@ describe("plugin manifest validation", () => { expect(JSON.stringify(manifest.scumLiveData).toLowerCase()).not.toMatch(/select\s+.+from|sqlite:\/\/|mysql:\/\/|password|credential|socket|hostpath/); }); + it("omits unverified SCUM typed RCON templates and gift catalogs", () => { + const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin"); + const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as GamePluginManifest & { scumLiveData: SCUMLiveDataManifestDeclaration }; + const liveData = manifest.scumLiveData; + const gates = new Map(liveData.capabilityGates.map((gate) => [gate.capability, gate])); + expect(liveData.typedRconTemplates ?? []).toEqual([]); + expect(liveData.giftCatalogs ?? []).toEqual([]); + expect(gates.get("economy-command.write")).toMatchObject({ gate: "disabled", evidenceStatus: "missing" }); + expect(gates.get("gift-command.write")).toMatchObject({ gate: "disabled", evidenceStatus: "missing" }); + expect((manifest.assetFiles ?? []).map((file) => file.path).filter((assetPath) => /assets\/scum-live\/(?:rcon|gifts)\//.test(assetPath))).toEqual([]); + expect(JSON.stringify(liveData).toLowerCase()).not.toMatch(/#setcurrency|#setfame|sendchat|commandtext|rawcommand|starter-pack|bandage|water-bottle|improvised-spear/); + }); + it("packages SCUM live read query assets with exact bounded result schemas", () => { const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin"); const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as GamePluginManifest & { scumLiveData: SCUMLiveDataManifestDeclaration }; @@ -531,6 +544,18 @@ describe("plugin manifest validation", () => { expect(errors.some((error) => error.includes("transportTemplateKeys") && error.includes("missing-template"))).toBe(true); }); + it("rejects SCUM typed RCON templates without conclusive confirmation schemas", () => { + const errors = validateTemporaryScumCompanionManifest((manifest, fixtureDir) => { + const assetPath = "assets/scum-live/rcon/unverified-gift.json"; + manifest.assetFiles = [...(manifest.assetFiles ?? []), { path: assetPath, mode: 384 }]; + writeFixtureJSON(fixtureDir, assetPath, { packaged: true }); + writeFixtureJSON(fixtureDir, "schemas/scum-live/unverified-gift.payload.schema.json", bridgeObjectSchema({ playerId: { type: "string", minLength: 1, maxLength: 96 } }, ["playerId"])); + writeFixtureJSON(fixtureDir, "schemas/scum-live/unverified-gift.result.schema.json", bridgeObjectSchema({ outcome: { enum: ["succeeded", "failed", "unknown"] } }, ["outcome"])); + manifest.scumLiveData.typedRconTemplates = [{ key: "unverified-gift", adapterVersion: "scum-live-data-v1", assetPath, digest: sha256FixtureDigest(fixtureDir, assetPath), capability: "gift-command.write", requiredSchemaFingerprint: currentSCUMSchemaFingerprint, transportKey: "scum-management", targetKey: "scum-management", permission: "server.game-client.command", payloadSchemaRef: "schemas/scum-live/unverified-gift.payload.schema.json", resultSchemaRef: "schemas/scum-live/unverified-gift.result.schema.json", timeoutMs: 5000, maxPayloadBytes: 2048 }]; + }); + expect(errors.some((error) => error.includes("confirmationSchemaRef"))).toBe(true); + }); + it("declares SCUM install/update and start lifecycle through plugin assets", () => { const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin"); const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as any;