Revert SCUM real data management change
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-11
|
||||
@@ -1,183 +0,0 @@
|
||||
## Context
|
||||
|
||||
The current SCUM implementation has two disconnected local player models and an unimplemented real-data path. Login ingestion can upsert `GamePlayer` only after Run has already supplied synthetic `eventType/playerId/playerName` fields, while the repository's only SCUM parser recognizes an invented `SCUM LOGIN/LOGOUT` console format rather than the game's real login file. The plugin manifest declares six SQLite query keys but contains no query asset, workflow dispatch changes local statuses without creating read jobs, and job completion never calls the SCUM row-ingestion service. The page therefore renders empty local objects, fabricated maintenance/backup references, hard-coded actions, a non-georeferenced dot board, and a hard-coded gift.
|
||||
|
||||
The reference `scum_robot` demonstrates the useful direction—tail the current server's login log, create a server-scoped local player, and let a machine-side process query its current SCUM.db—but its static SCUM structs are not migrated or authoritative, its production roster/world refresh mostly parses administrator-command output, and its raw SQL, permissions, XML rewrite, early gift-success recording, and forced player death are unsafe to copy.
|
||||
|
||||
This change crosses `platform/`, `platform_web/`, the SCUM plugin and SDK, and the independent Run repository. It must preserve the repository boundary: the SCUM plugin owns game-specific formats and declarations; Platform owns authorization, durable local business data, jobs, and safe APIs; Run owns generic machine-side execution and observed runtime facts. Browser code never receives SQL, raw XML, database paths, credentials, sockets, or raw RCON.
|
||||
|
||||
The implementation is blocked from declaring production SQL or `855` semantics until a read-only probe against the current bound service produces evidence. An unbound copied/reference database, historical cache, or generated model is only a hypothesis source. Run may create an execution-time read-only snapshot of the active database only when the snapshot is fenced to the current binding/database identity, timestamped, checksummed, short-lived, and invalidated on rebinding or schema change.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Replace SCUM projection/observation/Workflow product concepts with automatic synchronization into dedicated local records.
|
||||
- Create player identity and session data from authentic SCUM login/logout logs and enrich it only with verified current-service database facts.
|
||||
- Make the plugin own versioned parsers, SQL, result contracts, map metadata, RCON templates, and controlled mutation metadata while Run remains generic.
|
||||
- Provide trustworthy user, squad, map, gift, and AI surfaces with explicit effective-user permissions and no fabricated facts or evidence.
|
||||
- Keep unknown values null/absent, preserve the last successfully synchronized rows after a failed sync, and show ordinary connection/last-sync information without projection/freshness jargon or manual refresh actions.
|
||||
- Make manual and AI-originated writes use one typed authorization, review, dispatch, and confirmation path.
|
||||
- Preserve required server deployment, metadata, and administrator controls after removing the `管理` tab.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Do not add a `run/` tree, billing, cloud-host sales, agent-provider workflows, or unrelated marketplace behavior.
|
||||
- Do not use a repository/reference/cached `SCUM.db`, trust `scum_robot/model/scum_server.go` as a production schema, or ship guessed joins, enum values, coordinates, ownership, balances, or default zero values.
|
||||
- Do not expose arbitrary SQL, raw RCON, host paths, direct sockets, SCUM.db credentials, or raw `template_xml` to Platform Web or AI.
|
||||
- Do not hardcode SCUM tables, query keys, commands, or lifecycle behavior in Platform or Run.
|
||||
- Do not provide a Workflow status page, refresh/audit buttons, a manual synchronization API, or product-facing projection/observation terminology.
|
||||
- Do not automatically kill/respawn a character, invent a missing XML skill, or claim a write/gift succeeded before conclusive execution and confirmation evidence exists.
|
||||
- Do not promise a sub-second realtime map when the verified database or companion source cannot supply that cadence.
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Bootstrap a generic probe, then treat current-service discovery as a release gate
|
||||
|
||||
The first cross-repository prerequisite is to confirm or add the minimal generic, bounded diagnostic probe executor in the independent Run repository and deploy that compatible Run to the current binding. Platform and Run must establish the probe request/result envelope, binding fence, safe limits, and redaction without introducing any SCUM-specific table or path knowledge.
|
||||
|
||||
Only then will the production implementation execute bounded, read-only probes through the currently bound Run. For operator-directed discovery, a server-local diagnostic script may read the active database in place on the game server host when it is bounded, query-only, redacted, and recorded as diagnostic evidence rather than a Platform/plugin/browser data path. In both cases, the evidence must include `sqlite_master`, `PRAGMA table_info`, foreign-key/index information for candidate tables, small redacted samples, relevant enum distributions, coordinate ranges/cadence, and read-lock behavior. It must confirm player/profile/entity joins, squad rank meaning, flag ownership relationships, vehicle identity fields, currency types, and whether/where a mutable profile XML payload actually exists.
|
||||
|
||||
Current-service schema, log structure, and content-feature baselines are an explicit prerequisite for further adapter or write declarations. The change stores a redacted all-table schema inventory, process-adjacent log pattern inventory, XML tag/attribute/value-shape summaries, and log skeleton marker sets under `evidence/`; capability work must consume those local baselines rather than repeatedly probing piecemeal or asking the operator for raw XML/log/database files.
|
||||
|
||||
No production query or mutation asset may be merged until the probe evidence is captured in the change or an explicitly referenced test artifact. The probe is diagnostic-only, is not exposed as a browser action, and performs no write.
|
||||
|
||||
Alternative considered: start with the reference structs and fix queries after deployment. Rejected because the reference models are incomplete hypotheses and the current implementation already failed by treating guessed fields as facts.
|
||||
|
||||
### 2. Package game-specific behavior as signed plugin assets
|
||||
|
||||
The SCUM plugin will contain versioned assets for:
|
||||
|
||||
- login/logout parsing and field mapping;
|
||||
- read-only SQLite queries for player details, squads, squad members, vehicles, flags, and positions;
|
||||
- result JSON schemas and schema-version compatibility;
|
||||
- map identity, asset reference, world bounds, and coordinate transform;
|
||||
- RCON command templates for supported economy and gift operations;
|
||||
- database/XML mutation declarations for fields without a safe game command.
|
||||
|
||||
Manifest declarations will reference each asset and its digest. The generated Run package will contain those immutable assets. Platform queues a template key plus bounded parameters and expected digest; it does not construct SCUM SQL or command text. Run verifies the packaged asset/digest and executes it through a generic capability.
|
||||
|
||||
Alternative considered: make Run implement the six `scum.*` keys. Rejected because it makes Run game-aware and requires a Run release for every SCUM schema change. Alternative considered: accept SQL from the browser or a general Platform endpoint. Rejected because it creates an arbitrary database interface.
|
||||
|
||||
### 3. Add generic bounded Run contracts instead of SCUM workflows
|
||||
|
||||
The external Run contract will support:
|
||||
|
||||
- a generic current-target schema probe;
|
||||
- read-only SQLite template execution with parameter binding, a single SELECT/CTE or approved introspection statement, query-only mode, timeout, row/result-size limits, and no `ATTACH`, extension loading, or mutation;
|
||||
- plugin-owned typed RCON template execution;
|
||||
- plugin-owned guarded SQLite/XML mutation execution in a bounded transaction;
|
||||
- typed result envelopes containing server/plugin binding, template key, asset digest, schema version, observed time, checksum, rows or affected-row count, and a safe error/result code.
|
||||
|
||||
Platform will use ordinary durable Run jobs and the existing channel priority/lease/fencing rules. A new terminal-job hook validates the result envelope and schema, then invokes the matching local sync or write-confirmation service. There is no SCUM Workflow instance/step scheduler.
|
||||
|
||||
Alternative considered: repair the current SCUM workflow engine. Rejected because synchronization is a background data-ingestion concern, direct resource mutations already have a single target operation, and the workflow layer currently adds user-visible states without performing work.
|
||||
|
||||
### 4. Persist SCUM facts in dedicated normalized local stores
|
||||
|
||||
The local model will use dedicated repositories for at least:
|
||||
|
||||
- `scum_players` with unique `(server_instance_id, external_player_id)`;
|
||||
- `scum_player_sessions` with durable source-event identity;
|
||||
- `scum_player_details` for nullable profile/economy/squad facts and source checksum/time;
|
||||
- `scum_squads` and `scum_squad_members`;
|
||||
- `scum_vehicles`, `scum_flags`, and `scum_positions`;
|
||||
- `scum_gift_packages`, `scum_gift_items`, and `scum_gift_deliveries`;
|
||||
- internal `scum_sync_cursors` and successful sync generations.
|
||||
|
||||
High-frequency SCUM data will not be appended to the global metadata snapshot. The `mysql` backend will use normalized MySQL tables and migrations. The default `file` backend will use a platform-owned SQLite sidecar under the configured data directory; the `memory` backend will use an in-memory implementation for tests. The storage interface remains under `platform/repo`, database models/migrations under their fixed platform directories, and domain/API types remain separate.
|
||||
|
||||
A full successful resource scan uses a generation marker: rows are upserted transactionally, and records missing from the completed generation can be marked absent. A partial or failed scan never deletes or overwrites the last successful rows. Missing source fields remain `NULL`; zero is stored only when the source explicitly reports zero.
|
||||
|
||||
Alternative considered: retain the existing generic snapshot repositories and rename projection types. Rejected because every coordinate update would rewrite the whole metadata snapshot and would preserve the misleading data architecture the change is intended to remove.
|
||||
|
||||
### 5. Synchronize automatically from logs, binding state, and declared cadence
|
||||
|
||||
Run tails the plugin-declared login source and applies the plugin parser locally. Each typed login/logout event carries both a transport cursor `(source identity, stream generation, sequence)` for acknowledgement/resume and a privacy-safe logical event identity that is native to the log when available or derived from normalized non-network event fields and remains stable across rotation-overlap replay. A successful login atomically upserts the player, updates the current display name and last-seen time, and opens a session; logout closes the matching session without regressing a newer session. The source contract handles file rotation, truncation, Run restart, partial lines, and duplicate/overlapping batches without duplicating a logical event. Malformed lines, failed-login attempts, and events from an obsolete binding never create a player. Raw IP/network material is discarded before durable storage or logical fingerprinting.
|
||||
|
||||
The sync scheduler starts an initial bounded database scan when a compatible Run binding becomes ready, schedules resource scans at plugin-declared intervals with jitter and per-server concurrency limits, and may enqueue a bounded player-detail lookup after a new login. Positions use the measured safe cadence. If SCUM.db cannot provide the required map cadence, the plugin must declare a companion position-event source; the product must not compensate with fabricated motion or aggressive unsafe polling.
|
||||
|
||||
Platform Web always reads local resource APIs and receives updates through a platform-owned event stream/SSE. Opening a page never dispatches a database query, and there is no user refresh button.
|
||||
|
||||
Alternative considered: query Run on every page load. Rejected because it couples browser latency to machine connectivity, multiplies database reads, and makes pagination/filtering dependent on a remote SQLite file.
|
||||
|
||||
### 6. Replace the player-intelligence model with a lean roster
|
||||
|
||||
The SCUM product flow retains only the server-scoped player identity, current display name, first/last seen times, online session, login history, and verified player details. It removes alias-history, access-attempt, shared-IP, automatic-risk, and player-intelligence features from SCUM manifests, APIs, UI, and persistence when they have no non-SCUM consumer.
|
||||
|
||||
List APIs operate on the local store and support bounded pagination, search by name/external ID, online state, squad filter, and allowlisted sorting. Player detail returns nullable verified facts and last synchronized time. The UI uses a table and detail/edit surface rather than a fixed list of cards and hard-coded increments.
|
||||
|
||||
Alternative considered: keep the intelligence records hidden. Rejected because the user requested a smaller player model and the hidden ingestion side effects would still complicate login creation and retention.
|
||||
|
||||
### 7. Use one controlled-write path for manual and AI requests
|
||||
|
||||
Existing permission families remain explicit:
|
||||
|
||||
- `server.game-client.read` for local SCUM reads;
|
||||
- `server.game-client.command` for declared RCON/economy/gift actions;
|
||||
- `server.game-client.maintenance` plus an explicit synchronous danger confirmation for database/XML mutations.
|
||||
|
||||
The plugin page host must receive the current session's effective server permissions, not merely the permissions declared by the plugin/page. The backend reauthorizes every request and remains authoritative. An AI draft records the initiating user and cannot be dispatched unless that same confirming user still has the permission required for a manual request. AI never runs under an independent component write principal.
|
||||
|
||||
Fame and currencies use plugin-declared game commands when verified. Character attributes use a version-scoped XML patch declaration only after the active adapter confirms the actual profile XML source: Run reads the current payload, checks the expected row/checksum, changes only allowlisted named attributes or existing skill nodes, preserves all unknown nodes/attributes, updates at most one row with bound parameters, and reads back for confirmation. A real backup reference, verified offline/maintenance condition when required, before values, reason, idempotency key, and explicit confirmation are mandatory. A timestamp-shaped fake backup or maintenance string is invalid. There is no separate platform-admin approval domain, audit-initiation action, or approval queue.
|
||||
|
||||
`855` is not a field key. It can be declared only as a preset that expands to explicitly named attributes after its exact mapping is confirmed. Any respawn/death action needed to activate values is a separate explicit destructive operation and is never part of the default attribute save.
|
||||
|
||||
Alternative considered: reuse `player.attribute.855.set` and a generic integer field mutation. Rejected because the source is XML containing several named values. Alternative considered: let AI or Platform generate raw SQL/XML. Rejected because it leaks protected content and bypasses plugin version fencing.
|
||||
|
||||
### 8. Model gifts as local business data with typed delivery
|
||||
|
||||
Gift packages and items are server/plugin-instance scoped and have normal create, read, update, and delete operations, bounded eligibility/quantity rules, and explicit active state. A delivery freezes the target player and item definitions, atomically reserves any period-limited entitlement, uses a server-scoped idempotency key, and dispatches only a plugin-declared typed command. In-progress and unknown outcomes retain their reservation so concurrent requests cannot exceed the limit; only a conclusive non-executed failure may release it. The API reports creation/dispatch separately from delivered success. `delivered` is stored only after conclusive Run success; failed and unknown outcomes retain safe evidence and never auto-redeliver.
|
||||
|
||||
Alternative considered: preserve the current hard-coded starter package and notification. Rejected because neither is backed by catalog data. Alternative considered: copy `scum_robot`'s Redis command list. Rejected because it stores success before execution and accepts raw game command strings.
|
||||
|
||||
### 9. Make the SCUM surface contain exactly five tabs
|
||||
|
||||
The server detail order is `用户管理`, `队伍管理`, `实时地图`, `礼包管理`, `AI 助手`; the first plugin page is the default. `Workflow 状态` is removed from the manifest and the generic `管理` entry is excluded for the SCUM detail view. Deployment remains reachable from server-list actions. Server name and administrator membership move into a server-settings drawer opened from the detail header.
|
||||
|
||||
All copy and actions referring to projection, observation, freshness/staleness, refresh projection, refresh real data, initiate audit, pending review counts, or Workflow state are removed. Pages may show ordinary connection status and last synchronized/collected time. Empty states describe the missing business record without offering a manual refresh or implying generated data.
|
||||
|
||||
AI configuration keeps the existing reviewable config-diff path. Player-operation AI suggestions produce the same named-field edit draft used by the manual form.
|
||||
|
||||
Alternative considered: hide the two tabs without moving their content or changing the default route. Rejected because it would strand required server controls and leave invalid route fallbacks.
|
||||
|
||||
### 10. Keep legacy completed changes historical instead of importing obsolete specs
|
||||
|
||||
The repository's main OpenSpec baseline currently contains no SCUM capabilities, while the changes that introduced player intelligence, trajectories, the versioned gift catalog, player-state patching, projection/Workflow automation, controlled operations, and the old product surface are complete but unarchived. This replacement therefore declares only uniquely named `ADDED` capabilities. It supersedes those historical artifacts through explicit implementation deletion and acceptance coverage; it does not first archive them into the baseline and then attempt to remove them.
|
||||
|
||||
The old completed changes must not later be archived in a way that publishes their obsolete SCUM requirements as current baseline specs. Historical cleanup is a separate reviewed consolidation after this replacement is implemented, using skip-specs or an equivalent approach that preserves history without resurrecting removed behavior.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Risk] SCUM schema changes between game versions. → Require live probe evidence, schema-versioned assets, exact result schemas, digest fencing, and a disabled/incompatible state rather than fallback queries.
|
||||
- [Risk] Reading the live SQLite file can block or observe inconsistent rows. → Use query-only connections, short timeouts, bounded scans, measured cadence, jitter/backoff, and keep the last completed generation on failure.
|
||||
- [Risk] Game saves may overwrite direct database/XML changes. → Verify offline/maintenance semantics against the current service, require before/checksum guards and backup evidence, and disable unverified mutations.
|
||||
- [Risk] XML reserialization can destroy fields unknown to the current plugin. → Patch only named attributes/existing nodes while preserving raw document structure, then confirm by readback; never synthesize missing skills.
|
||||
- [Risk] Cross-repository Run support can lag the browser release. → Gate synchronization and write UI on compatible asset-executor capabilities and deploy Run support before enabling the new pages.
|
||||
- [Risk] Dedicated local tables add a second persistence path for the file backend. → Keep it behind a narrow `SCUMStore` interface, use explicit migrations and health checks, and fail the SCUM feature closed without affecting core server lifecycle.
|
||||
- [Risk] Position polling can generate large write volume or still not be realtime. → Measure source cadence, retain only current positions by default, make history explicitly bounded, and use a declared companion event source when database cadence is inadequate.
|
||||
- [Risk] Removing `管理` can make server settings undiscoverable. → Add header-level server settings and preserve server-list deployment actions before removing the tab.
|
||||
- [Risk] Old projection/Workflow snapshot data can look real after upgrade. → Never migrate it into the new tables; populate only from post-upgrade authenticated logs/current-service sync.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Confirm or implement the minimal generic diagnostic probe contract in the independent Run repository, deploy a compatible Run to the active binding, and verify binding fencing/redaction without adding SCUM-specific behavior to Run.
|
||||
2. Probe the active service database in read-only mode, using either the durable Run path for product acceptance or an explicitly operator-directed server-local diagnostic for discovery only; capture schema/cadence/lock evidence and confirm whether the `855` preset has a real named-attribute mapping. Stop any affected capability if the evidence cannot support safe query or mutation declarations.
|
||||
3. Add plugin/SDK/validator contracts and immutable SCUM parser/query/map/command/mutation assets backed by that evidence; implement and deploy the remaining compatible generic Run executors in the independent repository.
|
||||
4. Add the dedicated SCUM store implementations, migrations, health checks, local resource contracts, and automatic sync scheduler behind a disabled-by-default compatibility gate.
|
||||
5. Wire authentic, rotation-safe login events and terminal Run query results into local transactional upserts; verify initial and periodic sync against the current service without enabling writes.
|
||||
6. Build the player, squad, map, and gift APIs/pages, effective-permission context, server-settings relocation, and preserved AI configuration flow against the new local records.
|
||||
7. Enable controlled RCON writes, then separately enable guarded XML mutations only after actual source mapping, backup/offline/readback verification, effective permission, and explicit confirmation succeed on the current service.
|
||||
8. Cut navigation and clients to the five new tabs; remove Workflow/projection/observation APIs, services, repositories, snapshot fields, page actions, banned copy, fake gifts/map/actions, and obsolete tests/spec assumptions.
|
||||
9. Run focused backend/frontend/plugin tests, external Run contract acceptance, browser acceptance against real synchronized data, `scripts/check-structure.sh`, and `openspec validate replace-scum-projections-with-real-data-management --strict` before marking tasks complete.
|
||||
|
||||
Rollback disables the SCUM compatibility gate and write actions while leaving the new local tables for diagnosis. It must not re-enable fake projection data or migrate new facts back into old Workflow/projection records. Core server lifecycle, logs, jobs, and AI provider management remain available.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- What are the exact current-service tables, columns, joins, enum values, coordinate cadence, and safe read behavior?
|
||||
- What exact named attribute mapping does the operator mean by the `855` preset, and what game action—if any—is required for those values to take effect?
|
||||
- Does the verified current SCUM version support safe RCON readback for fame/currencies, or must confirmation use a database query?
|
||||
- Which map asset/version and coordinate transform are authorized for first-party redistribution?
|
||||
- Which gift item aliases and delivery transports can be verified without exposing or accepting arbitrary commands?
|
||||
- Which pure-Go SQLite driver and migration mechanism will be pinned for the default file-backend sidecar after dependency and build verification?
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
# Run Guarded SQLite/XML Mutation Execution Handoff (2026-08-13)
|
||||
|
||||
This is a browser-repository handoff for a separately authorized task in the independent Run repository `git@git.npc0.com:admin343/run.git`. It is not Run implementation or deployment evidence, and it does not enable SCUM database/XML write capabilities by itself.
|
||||
|
||||
## Positive prompt (正向提示词)
|
||||
|
||||
Implement generic plugin-owned guarded single-row SQLite/XML mutation execution for Run's protected SQL capability so Platform can dispatch reviewed SCUM profile-XML writes without sending raw SQL, raw XML, browser mutation text, host paths, sockets, credentials, table/column overrides, raw rows, `fieldKey=855`, or undeclared patch fields. Success means Run accepts only a typed leased `guardedMutation` request containing logical target/template keys, adapter/schema version, immutable asset digest, target identity digest, expected row/value/XML digests, patch digest, backup/offline/danger-confirmation evidence digests, readback expectation digest, idempotency key, bounded scalar payload, safe review reason, and strict limits; applies the packaged preserving patch inside a bounded transaction; rolls back on guard mismatch, malformed XML, zero/multiple affected rows, or failed readback; and returns a typed `sqlite.guarded-mutation` terminal envelope that Platform can validate against the original durable job.
|
||||
|
||||
## Directional prompt (方向提示词)
|
||||
|
||||
Work only in the independent Run repository. Preserve Run as a generic executor: resolve protected SQLite targets and packaged mutation templates from the generated Run workspace, verify active binding, adapter/schema fingerprint, asset/target/guard/patch/backup/offline/confirmation/readback digests, validate scalar payload against packaged schema, open only the package-declared SQLite target or fenced current-service snapshot as permitted by the generated plan, enforce timeout/busy/readback/payload limits, execute a bounded transaction, use a preserving XML patcher that changes only allowlisted named attributes or existing nodes, preserve unknown XML content, reject absent undeclared nodes instead of synthesizing them, update exactly one guarded row, and read back conclusive digest evidence before success.
|
||||
|
||||
Expected Run verification: focused protocol/runtime tests for valid single-row mutation, unknown template, digest mismatch, stale expected row/value/XML guard, missing backup/offline/danger confirmation, malformed XML, absent node, zero-row update rollback, multi-row rollback, timeout/cancellation, readback mismatch/unknown, result-size limit, duplicate/late terminal result behavior, and `go test ./...` from the Run repository root. After implementation, record the tested Run commit, distribution/deployment evidence for the active binding, and safe terminal-envelope evidence back in this browser-repository change before enabling guarded mutation gates or declaring production XML mutation assets.
|
||||
|
||||
## Boundary prompt (任务边界)
|
||||
|
||||
Do not edit or vendor Run source into this browser repository, add a `run/` tree here, accept raw SQL/XML/browser mutation text, expose host paths/credentials/sockets/raw XML/SQL/IPs/player identities in evidence, add SCUM-specific executor branches, branch on SCUM keys/tables/profile names/`855` semantics, synthesize missing XML nodes, rewrite XML from incomplete structs, update more than one row, skip same-instance backup/offline/danger-confirmation/readback guards, enable `855`, enable production guarded mutation assets, or treat this handoff as product acceptance evidence. Run must remain game-agnostic and execute only package-declared generic assets under the active signed binding and lease.
|
||||
|
||||
## Browser-side frozen contract
|
||||
|
||||
- Platform domain/DTO contracts define `SCUMGuardedMutationRequest`, `SCUMGuardedMutationResult`, guarded mutation bounds, terminal statuses, mutation readback statuses, and stable safe error codes for guard/readback/rollback failures.
|
||||
- Job-channel DTOs expose leased `executionInput.guardedMutation` to Run and parse terminal `executionResult.guardedMutation` from Run.
|
||||
- Validators reject unsafe template keys, protected material, raw XML, SQL/path-like material, `855` field payloads, missing schema/asset/identity/value/XML/patch/backup/offline/danger-confirmation/readback digests, loose affected-row bounds, unsafe review reasons, unsafe summaries, multi-row success, and missing conclusive readback.
|
||||
- Service job completion accepts `sqlite.guarded-mutation` only for `remote.run.protected.sql` jobs carrying a typed guarded mutation request, requires the typed result on success, checks leased job/binding/template/schema/asset/target/guard/patch/backup/offline/confirmation/readback identity, and includes typed result digests in terminal idempotency fingerprints.
|
||||
|
||||
## Remaining evidence required before enabling mutations
|
||||
|
||||
- Tested Run commit and `go test ./...` output from the independent Run repository.
|
||||
- Generated Run package carrying any future packaged mutation template, preserving XML patcher declaration, schema/payload/result/readback schemas, immutable asset digests, and logical SQLite target binding.
|
||||
- Current-service evidence proving the exact source row/XML payload, named attribute mapping, preserving patch semantics, same-instance backup/restore evidence, offline/maintenance requirements, readback behavior, and safe failure outcomes.
|
||||
- Active binding deployment evidence showing compatible Run executes `remote.run.protected.sql` through the typed guarded envelope without raw SQL/XML exposure.
|
||||
- Platform acceptance evidence against isolated test data or an explicitly authorized test player proving permission, explicit confirmation, guards, backup/offline requirements, idempotency, readback, unknown-result handling, XML preservation, and no implicit respawn/death/kick activation.
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
# Run Log-Source Tailing Handoff (2026-08-13)
|
||||
|
||||
This is a browser-repository handoff for a separately authorized task in the independent Run repository `git@git.npc0.com:admin343/run.git`. It is not Run implementation or deployment evidence, and it does not enable SCUM login-driven player creation by itself.
|
||||
|
||||
## Positive prompt (正向提示词)
|
||||
|
||||
Implement or extend generic plugin-declared log-source tailing in Run so Platform can consume sanitized parsed login/logout events from the current bound SCUM service without sending or receiving raw log paths, raw log lines, IP/network material, sockets, credentials, SQL, XML, or SCUM-specific executor branches. Success means Run tails only package-declared logical log sources, persists transport cursor state, handles rotation, copy-truncate, restart/resume, zero-byte active files, partial-line buffering, parser digest fencing, logical event fingerprinting, and replay, then returns typed `log.parsed-events` terminal envelopes or later parsed-event ingress payloads that Platform can validate against the active binding and declared parser identity.
|
||||
|
||||
## Directional prompt (方向提示词)
|
||||
|
||||
Work only in the independent Run repository. Preserve Run as a generic executor: resolve `file.tail` sources from the generated Run package and runtime bindings, keep host paths and globs local to Run, fence every parser by package asset digest and parser digest, decode lines according to the packaged parser declaration, buffer incomplete lines across polling/restart boundaries, detect rotation or truncate by source fingerprint/generation rather than SCUM file names, persist acknowledged cursor state `(source identity digest, stream generation, sequence)`, and emit sanitized logical events whose stable `logicalEventDigest` excludes source identity, stream generation, sequence, IP/network material, and coordinates.
|
||||
|
||||
Expected Run verification: focused protocol/runtime tests for zero-byte active file startup, append after empty file, UTF-16LE decoding, partial line buffering, oversized/undecodable/malformed line handling, failed-login discard, rotation, copy-truncate overlap, restart/resume from acknowledged cursor, duplicate transport cursor rejection, duplicate logical event replay, parser digest mismatch, stale binding/source identity rejection, bounded batch limits, safe error codes, and `go test ./...` from the Run repository root. After implementation, record the tested Run commit, distribution/deployment compatibility evidence for the active binding, and a safe redacted terminal-envelope sample back in this browser-repository change before enabling login-event ingestion tasks.
|
||||
|
||||
## Boundary prompt (任务边界)
|
||||
|
||||
Do not edit or vendor Run source into this browser repository, add a `run/` tree here, expose raw host paths, resolved log names, glob patterns, log lines, IP/network identifiers, sockets, credentials, SQL, XML, raw player identities, or parser-internal source paths in Platform, browser, AI context, or evidence. Do not branch Run behavior on SCUM file names, SCUM table names, SCUM parser keys, or SCUM event semantics; do not treat ordinary log ingest as a parsed-event success path; do not fabricate players/sessions from malformed, failed-login, duplicate, obsolete-binding, or out-of-order events; and do not treat this handoff as product acceptance evidence.
|
||||
|
||||
## Browser-side frozen contract
|
||||
|
||||
- Platform domain/DTO contracts define `SCUMParsedLogBatchResult`, `SCUMParsedLogEvent`, `SCUMParsedLogCursor`, parsed-log batch limits, tail states (`advanced`, `rotated`, `truncated`, `restarted`, `partial-buffered`, `replayed`), and safe terminal statuses.
|
||||
- Job-channel DTOs parse terminal `executionResult.parsedLogBatch` from Run when `executionResult.kind` is `log.parsed-events`.
|
||||
- Validators require request/job/binding identity, source/stream/parser key, adapter version, parser asset digest, parser digest, first/last redacted source identity and generation cursors, bounded event counts, logical event digests, event/payload digests, safe scalar payloads, and applied limits.
|
||||
- Validators reject raw log/path/IP/network material, raw XML, SQL-like text, unsafe summaries, unsafe safe-error messages, duplicate transport cursors, duplicate logical event digests within a batch, event-count mismatches, loose line/payload/result bounds, parser digest omissions, and cross-generation batch success.
|
||||
- Service job completion accepts `log.parsed-events` only for `logs.backfill` jobs carrying a leased declared `file.tail` log source, checks server/Run endpoint, declared source/stream key, plugin id/version when frozen, parser key/version/digest/adapter version when frozen in execution inputs, and single source identity/generation boundaries.
|
||||
- Terminal idempotency fingerprints include parsed-log result digest, asset digest, parser digest, stream generation, event count, and tail state.
|
||||
|
||||
## Remaining evidence required before enabling parsed-login ingestion
|
||||
|
||||
- Tested Run commit and `go test ./...` output from the independent Run repository.
|
||||
- Generated Run package evidence carrying the declared `file.tail` source, parser asset, parser digest, max-line limit, cursor policy, and privacy policy.
|
||||
- Active current-service evidence showing the latest zero-byte login file, non-empty UTF-16LE fixtures, rotation/truncate behavior, and sanitized parser output with no raw network material.
|
||||
- Platform acceptance evidence proving parsed batches are authenticated, fenced to the current server/Run/plugin/parser binding, idempotent across replay, and rejected for stale parser digest, stale source identity, duplicate transport cursor, and unsafe payloads.
|
||||
- Follow-on Platform ingestion tests from task 6.1 proving successful login creates one local player/session, logout closes only matching sessions, and malformed/failed/duplicate/out-of-order events do not fabricate state.
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
# Run SQLite Template Execution Handoff (2026-08-13)
|
||||
|
||||
This is a browser-repository handoff for a separately authorized task in the independent Run repository `git@git.npc0.com:admin343/run.git`. It is not Run implementation or deployment evidence, and it does not enable SCUM database-backed reads by itself.
|
||||
|
||||
## Positive prompt (正向提示词)
|
||||
|
||||
Implement generic packaged SQLite-template execution for Run's `remote.run.db.sqlite.query` capability so Platform can dispatch current-service SCUM read jobs without sending SQL text, host paths, credentials, sockets, raw XML, raw RCON, or browser-supplied table names. Success means Run accepts only a typed leased `sqliteTemplate` request containing a logical target key, template key, adapter/schema version, immutable asset digest, canonical parameter digest, bounded scalar parameters, and strict limits; executes a query-only package-resolved SQLite template; and returns a typed `sqlite.template-query` terminal envelope that Platform can validate against the original durable job.
|
||||
|
||||
## Directional prompt (方向提示词)
|
||||
|
||||
Work only in the independent Run repository. Preserve Run as a generic executor: resolve package-scoped logical `databases/...` targets and packaged assets from the generated Run workspace, verify the asset digest and adapter/schema fingerprint, validate canonical bounded parameters, open SQLite in query-only/read-only mode or use a fenced short-lived read-only snapshot, enforce one-statement validation, reject mutation/DDL/`ATTACH`/extension loading/write PRAGMAs/multi-statement input, bind parameters, apply short busy and operation timeouts, honor cancellation, and enforce row/result-byte limits. Return the terminal envelope through the existing signed job-result channel with request/job/binding identity, capability, target/template key, adapter version, schema fingerprint, asset digest, parameter digest, source fingerprint, observed time, result digest, row count, bounded rows, truncation flag, applied limits, status, and stable safe error code.
|
||||
|
||||
Expected Run verification: focused protocol/runtime tests for valid template execution, digest mismatch, schema mismatch, parameter validation, cancellation, busy/timeout handling, result limits, one-statement enforcement, mutation/DDL/`ATTACH`/extension/write-PRAGMA rejection, duplicate/late terminal result behavior, and `go test ./...` from the Run repository root. After implementation, record the tested Run commit, distribution/deployment evidence for the active binding, and safe terminal-envelope evidence back in this browser-repository change before enabling DB-backed read gates.
|
||||
|
||||
## Boundary prompt (任务边界)
|
||||
|
||||
Do not edit or vendor Run source into this browser repository, add a `run/` tree here, download or parse `SCUM.db` on the platform/plugin/browser side, expose raw SQL/RCON/XML/paths/credentials/sockets/IPs/player identities in evidence, accept browser command/query text, add SCUM-specific executor branches, infer SCUM table semantics inside Run, enable write capabilities, enable database-backed read gates before tested Run evidence is recorded, or treat this handoff as product acceptance evidence. Run must remain game-agnostic and execute only package-declared generic assets under the active signed binding and lease.
|
||||
|
||||
## Browser-side frozen contract
|
||||
|
||||
- Platform domain/DTO contracts define `SCUMSQLiteTemplateRequest`, `SCUMSQLiteTemplateResult`, bounded template limits, scalar parameters/rows, and stable terminal statuses.
|
||||
- Job-channel DTOs expose leased `executionInput.sqliteTemplate` to Run and parse terminal `executionResult.sqliteTemplate` from Run.
|
||||
- Validators reject unsafe template keys, protected material, raw SQL/path-like values, unsupported capabilities, invalid digests, loose bounds, mismatched row counts, and unsafe result rows.
|
||||
- Service job completion accepts `sqlite.template-query` only for `remote.run.db.sqlite.query`, requires the typed result on success, checks leased job/binding/template/schema/asset/parameter identity, and includes typed result digests in terminal idempotency fingerprints.
|
||||
|
||||
## Remaining evidence required before enabling reads
|
||||
|
||||
- Tested Run commit and `go test ./...` output from the independent Run repository.
|
||||
- Generated Run package carrying the packaged query assets and immutable digests.
|
||||
- Active binding deployment evidence showing the compatible Run advertises and executes `remote.run.db.sqlite.query` through the typed envelope.
|
||||
- Platform acceptance evidence for at least one safe read-only template job with no raw SQL, host paths, credentials, sockets, raw XML, raw RCON, or browser-supplied query material.
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
# Run Typed RCON Template Execution Handoff (2026-08-13)
|
||||
|
||||
This is a browser-repository handoff for a separately authorized task in the independent Run repository `git@git.npc0.com:admin343/run.git`. It is not Run implementation or deployment evidence, and it does not enable SCUM write capabilities by itself.
|
||||
|
||||
## Positive prompt (正向提示词)
|
||||
|
||||
Implement generic plugin-owned typed RCON-template execution for Run's protected RCON capability so Platform can dispatch reviewed SCUM command writes without sending browser command text, raw RCON, SQL, XML, host paths, sockets, credentials, or undeclared command keys. Success means Run accepts only a typed leased `rconTemplate` request containing logical transport/target keys, template key, adapter/schema version, immutable asset digest, canonical payload digest, confirmation digest, target identity digest, idempotency key, bounded scalar payload, safe review reason, and strict limits; renders only the packaged template; executes through generic protected RCON; performs the declared confirmation path; and returns a typed `rcon.template-command` terminal envelope that Platform can validate against the original durable job.
|
||||
|
||||
## Directional prompt (方向提示词)
|
||||
|
||||
Work only in the independent Run repository. Preserve Run as a generic executor: resolve protected RCON transports and packaged command templates from the generated Run workspace, verify asset/payload/confirmation digests and active binding, validate scalar payload against packaged schema, render only the packaged template with bound values, enforce payload/response/confirmation limits, honor timeout and cancellation, redact rendered command text from all result envelopes, and return stable safe error codes. The terminal envelope must contain request/job/binding identity, capability, transport/target/template key, adapter version, schema fingerprint when required, asset digest, payload digest, confirmation digest, target identity digest, observed time, result digest, response digest, confirmation status, confirmation digest id, safe summary, safe error, and applied limits.
|
||||
|
||||
Expected Run verification: focused protocol/runtime tests for valid template execution, unknown template, digest mismatch, payload schema rejection, missing protected transport, timeout/cancellation, response limits, confirmation success/failure/unknown, duplicate/late terminal result behavior, and `go test ./...` from the Run repository root. After implementation, record the tested Run commit, distribution/deployment evidence for the active binding, and safe terminal-envelope evidence back in this browser-repository change before enabling typed command or gift gates.
|
||||
|
||||
## Boundary prompt (任务边界)
|
||||
|
||||
Do not edit or vendor Run source into this browser repository, add a `run/` tree here, accept browser command text, expose rendered RCON text, expose host paths/credentials/sockets/raw XML/SQL/IPs/player identities in evidence, add SCUM-specific executor branches, branch on SCUM keys/commands/tables/gift/economy semantics, enable typed RCON templates or gift catalogs in the production SCUM manifest, enable write gates before current-service command/readback evidence and tested Run evidence are recorded, or treat this handoff as product acceptance evidence. Run must remain game-agnostic and execute only package-declared generic assets under the active signed binding and lease.
|
||||
|
||||
## Browser-side frozen contract
|
||||
|
||||
- Platform domain/DTO contracts define `SCUMTypedRCONTemplateRequest`, `SCUMTypedRCONTemplateResult`, typed RCON bounds, terminal statuses, and confirmation statuses.
|
||||
- Job-channel DTOs expose leased `executionInput.rconTemplate` to Run and parse terminal `executionResult.rconTemplate` from Run.
|
||||
- Validators reject unsafe template keys, protected material, raw command-like payload keys, invalid digests, loose bounds, unsafe review reasons, unsafe summaries, unconfirmed success, and mismatched adapter/binding versions.
|
||||
- Service job completion accepts `rcon.template-command` only for `remote.run.protected.rcon` jobs carrying a typed template request, requires the typed result on success, checks leased job/binding/transport/template/schema/asset/payload/confirmation/target identity, and includes typed result digests in terminal idempotency fingerprints.
|
||||
|
||||
## Remaining evidence required before enabling writes
|
||||
|
||||
- Tested Run commit and `go test ./...` output from the independent Run repository.
|
||||
- Generated Run package carrying any future packaged RCON templates and immutable digests.
|
||||
- Current-service evidence proving supported command semantics, confirmation/readback behavior, item aliases/transports where relevant, and safe failure outcomes.
|
||||
- Active binding deployment evidence showing compatible Run executes `remote.run.protected.rcon` through the typed envelope without raw command exposure.
|
||||
- Platform acceptance evidence for at least one safe typed command job against isolated test data or an explicitly authorized test player.
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
# SCUM Capability Negotiation and Run Acceptance Audit (2026-08-13)
|
||||
|
||||
This evidence records a browser-repository audit for task 4.7 and the Platform-side implementation evidence for task 4.8.
|
||||
|
||||
## Run acceptance audit
|
||||
|
||||
- Local independent Run checkout inspected read-only from the ignored nested checkout: it is on `main...origin/main`, latest commit `8fe6f9b` (`Fix SQLite probe data target mapping`), with only ignored local-debug dirt. The visible history covers schema-probe and data-target materialization work (`6cb6ba3`, `9cc9ab3`, `8fe6f9b`) and does not contain the later generic SQLite-template, typed RCON-template, guarded mutation, or parsed-log-source execution acceptance evidence required by task 4.7.
|
||||
- Browser-repository handoff evidence files for SQLite template execution, typed RCON template execution, guarded SQLite/XML mutation execution, and log-source tailing were re-read. Each is explicitly a contract handoff only and says it is not Run implementation, deployment, or product acceptance evidence.
|
||||
- Server-management MCP was used for the remote path: `list_devices`, `test_connection` for `枣庄服务器`, then one bounded read-only PowerShell diagnostic. The diagnostic returned only redacted aggregates: connection OK, `runProcessCount=0`, zero scanned Run roots, and zero marker hits for `sqlite.template-query`, `rcon.template-command`, `sqlite.guarded-mutation`, `log.parsed-events`, duplicate/late terminal markers, and protected RCON/SQL capability markers. It emitted no host paths, command lines, log lines, credentials, sockets, IP/network material, database rows, SQL, XML, RCON text, or player identities.
|
||||
- Conclusion: task 4.7 remains pending. There is still no independent Run acceptance evidence proving channel priorities, leases, fencing, acknowledgements, idempotency, and late/duplicate terminal-result handling for the new generic capabilities. The schema-probe/data-target evidence recorded earlier remains valid only for the probe path and does not enable DB-backed reads, typed commands, gifts, guarded mutations, or parsed-login ingestion.
|
||||
|
||||
## Platform capability negotiation
|
||||
|
||||
- Added a read-only Platform capability negotiation path for the active server/plugin/Run endpoint/runtime binding: `GET /api/v1/server-instances/{id}/scum/capabilities`.
|
||||
- The negotiation evaluates each SCUM manifest gate independently against the bound Run capability list and latest accepted typed terminal evidence for the same server, Run binding, Run endpoint, plugin version, adapter version, database identity, schema fingerprint, and asset digest set.
|
||||
- The route returns only capability, enabled/disabled state, safe reason code, safe reason, binding identifiers, and evaluation time. It never dispatches Run jobs and never returns terminal rows, SQL, XML, RCON text, host paths, DSNs, sockets, credentials, protected payloads, or raw service content.
|
||||
- Added regression coverage for compatible schema-probe evidence, compatible `players.read` SQLite-template evidence, missing per-capability evidence, missing protected RCON executor support, stale Run binding rejection, unauthorized access, no forbidden-material leakage, and no dispatch from the capabilities read.
|
||||
|
||||
## Verification
|
||||
|
||||
- Passed: `go test ./domain ./dto ./service ./api -run 'TestSCUMCapabilityNegotiation|TestSCUMSchemaProbeEndpointQueuesPlatformScheduledDurableJob|TestLegacySCUMEndpointsReturnNotFoundWithoutDispatchingJobs'`.
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
# SCUM current-service content features baseline (redacted)
|
||||
|
||||
- diagnostic: scum-current-service-content-features-v1
|
||||
- observed_utc: 2026-08-13T03:56:41Z
|
||||
- target: `枣庄服务器` via server-management MCP
|
||||
- redaction: type, null, range, length, hash, XML-key, and log-skeleton features only; no raw rows, raw XML, raw log lines, host paths, IPs, credentials, sockets, or player identities
|
||||
|
||||
## DB content feature sources
|
||||
|
||||
This file complements the full schema inventory in `scum-current-service-db-schema-baseline-2026-08-13.md` and the join/range diagnostic in `scum-current-service-sqlite-diagnostic-2026-08-12.md`. The following previously captured content features remain authoritative inputs for adapter design:
|
||||
|
||||
- Identity/profile content: `user` has `74` rows, `user_profile` has `73` rows, all profiles join to `user`, and `72` profiles join to `prisoner` / `prisoner_entity` / `entity`; one profile has no current prisoner/entity.
|
||||
- XML-bearing columns: `user_profile.template_xml` is non-null for `73/73` profiles with length range `2541-2673`; `prisoner_skill.xml` has `72/1656` non-null values with length `15-65`; `item_entity.xml` has `42304/62844` non-null values with length `136-4781`.
|
||||
- Squad content: `squad` has `7` rows; `squad_member` has `18` rows; observed rank distribution is `1:6`, `2:2`, `3:4`, `4:6`, but rank meanings remain unknown.
|
||||
- Economy content: `bank_account_registry` has `73` account rows and `bank_account_registry_currencies` has `146` rows; observed currency types are numeric `1` and `2`, but labels/units and command confirmation remain unknown.
|
||||
- Coordinate content: joined prisoner positions, vehicle positions, base/base-element positions, and spawn-location coordinates have verified numeric ranges, but DB cadence did not prove sub-10s realtime updates.
|
||||
|
||||
## XML content structure
|
||||
|
||||
The XML diagnostic sampled only tag names, attribute names, parser success counts, and value-shape digests. It did not emit XML documents or attribute values.
|
||||
|
||||
| table | column | sampled | parse_ok | root_tags | element_tags | attribute_names | value_shape_digest |
|
||||
|---|---|---:|---:|---|---|---|---|
|
||||
| user_profile | template_xml | 73 | 73 | CharacterTemplate x73 | Skill x1533; CharacterTemplate x73 | Name x1606; Attribute x1533; ClassName x1533; Level x1533; Experience x1533; Strength x73; Constitution x73; Dexterity x73; Intelligence x73; Age x73; Gender x73; AppearanceIndex x73; TattooIndex x73; BodyHairIndex x73; HairStyleIndex x73; MoustacheStyleIndex x73; BeardStyleIndex x73; BreastSize x73; PenisSize x73; GrowOverTime x73; BirthDate x73; ArrestDate x73; FaceType x73; SkinTone x73; HairColor x73 | sha256:9f839093a598edf3 |
|
||||
| prisoner_skill | xml | 72 | 0 | - | - | - | sha256:e3b0c44298fc1c14 |
|
||||
| item_entity | xml | 100 | 100 | Item x100 | Item x100; ContinuousFuelConsumption x25; Component x17; Stacks x15; ContainedItemsMetadata x15; ItemComponents x11; Locks x6; ContinuousFuelConsumptions x5; HeatSource_0 x5; HeatSource_1 x5; HeatSource_2 x5; HeatSource_3 x5; HeatSource_4 x5 | _bloodStage x100; _dryBloodStage x100; _isCrafted x100; _uncraftTime x100; _lastAccessTime x100; _healthRatioWhenCrafted x100; UniqueId x25; CurrentEnergyConsumptionRate x25; HeatSourceID x25; DialValue x25; FuelConsumptionCreated x25; Name x17; _radiation x15; _weight x15; _weightUsed x15; _isPartOfEvent x15; WaterWeight x15; StacksCount x15; _owningUserProfileId x6; _activeAccessLevel x6; _pendingContainedItemsRadiationAmount x6; _forbiddenZoneEnterTimestamp x6; _protectingFlagId x6; _colorIndex x6; _name x6 | sha256:5a62ccfa73af3694 |
|
||||
|
||||
## XML conclusions
|
||||
|
||||
- `user_profile.template_xml` is the only sampled profile-level XML source that parses cleanly and exposes named character attributes plus skill entries.
|
||||
- The content features support future named-field adapter design for attributes such as `Strength`, `Constitution`, `Dexterity`, `Intelligence`, and existing `Skill` attributes, but they do not by themselves prove safe write semantics, backup/offline requirements, readback behavior, or a `855` preset mapping.
|
||||
- `prisoner_skill.xml` is not a valid XML document in the sampled current-service rows, so it cannot be treated as the preserving XML mutation source.
|
||||
- `item_entity.xml` parses as item metadata and is not a profile attribute source.
|
||||
|
||||
## Log content skeleton features
|
||||
|
||||
The log diagnostic sampled bounded prefixes from process-adjacent logs, grouped by filename pattern. Skeletons replace quoted text, numbers, network-shaped tokens, long hex identifiers, and ASCII words; marker sets record structural tokens only.
|
||||
|
||||
| filename_pattern | sampled_files | sampled_lines | bytes | encodings | marker_sets | skeleton_digest | top_skeletons |
|
||||
|---|---:|---:|---:|---|---|---|---|
|
||||
| admin_{digits}.log | 15 | 48 | 6816 | utf-16-le x15 | date x18; date,network x15; date,login-event x15 | sha256:ffaf5a692fdcf219 | date/network boilerplate x15; admin-login-event skeleton x15; admin-action skeleton x12 |
|
||||
| armor_absorption_{digits}.log | 15 | 15 | 1500 | utf-16-le x15 | date,network x15 | sha256:2c509fc280f438aa | shared date/network boilerplate x15 |
|
||||
| base_building_destruction_{digits}.log | 15 | 15 | 1500 | utf-16-le x15 | date,network x15 | sha256:2c509fc280f438aa | shared date/network boilerplate x15 |
|
||||
| chat_{digits}.log | 15 | 24 | 2706 | utf-16-le x15 | date,network x15; date x9 | sha256:8e42f8c5ba48c29b | shared date/network boilerplate x15; redacted chat skeleton x9 |
|
||||
| chest_ownership_{digits}.log | 15 | 15 | 1500 | utf-16-le x15 | date,network x15 | sha256:2c509fc280f438aa | shared date/network boilerplate x15 |
|
||||
| connection_log.txt | 5 | 100 | 82695 | utf-8-sig x5 | date,time x65; date,time,login-event x35 | sha256:80153240fb51639e | service connection event skeletons x35/x35/x10 |
|
||||
| connection_log_{n}.txt | 10 | 200 | 1248965 | utf-16-le x5; utf-8-sig x5 | none x100; date,time x90; date,time,network x5; date,time,login-event x5 | sha256:346312bbea656ecd | mixed encoded service connection skeletons |
|
||||
| economy_{digits}.log | 15 | 15 | 1500 | utf-16-le x15 | date,network x15 | sha256:2c509fc280f438aa | shared date/network boilerplate x15 |
|
||||
| event_kill_{digits}.log | 12 | 12 | 1200 | utf-16-le x12 | date,network x12 | sha256:2c509fc280f438aa | shared date/network boilerplate x12 |
|
||||
| famepoints_{digits}.log | 15 | 15 | 1500 | utf-16-le x15 | date,network x15 | sha256:2c509fc280f438aa | shared date/network boilerplate x15 |
|
||||
| gameplay_{digits}.log | 15 | 159 | 987486 | utf-16-le x15 | date x78; date,coordinates x66; date,network x15 | sha256:b8ae42ecfeb39fad | gameplay header skeleton x33; coordinate event skeletons x18/x18 |
|
||||
| kill_{digits}.log | 12 | 12 | 1200 | utf-16-le x12 | date,network x12 | sha256:2c509fc280f438aa | shared date/network boilerplate x12 |
|
||||
| login_{digits}.log | 12 | 21 | 3456 | utf-16-le x12 | date,network x12; date,login-event,coordinates,network x9 | sha256:e4f04da6e2da7470 | shared date/network boilerplate x12; login/logout coordinate skeletons x6/x3 |
|
||||
| loot_{digits}.log | 15 | 186 | 49164 | utf-16-le x15 | date x171; date,network x15 | sha256:36e500f92b6c82f3 | loot interaction skeletons x108/x18; shared date/network boilerplate x15 |
|
||||
| network_objects_{digits}.log | 15 | 15 | 1500 | utf-16-le x15 | date,network x15 | sha256:2c509fc280f438aa | shared date/network boilerplate x15 |
|
||||
| quests_{digits}.log | 15 | 15 | 1500 | utf-16-le x15 | date,network x15 | sha256:2c509fc280f438aa | shared date/network boilerplate x15 |
|
||||
| raid_protection_{digits}.log | 15 | 33 | 2868 | utf-16-le x15 | date x18; date,network x15 | sha256:80b32ec451bdbd18 | shared date/network boilerplate x15; raid protection status skeletons x9/x9 |
|
||||
| sentry_{digits}.log | 15 | 15 | 1500 | utf-16-le x15 | date,network x15 | sha256:2c509fc280f438aa | shared date/network boilerplate x15 |
|
||||
| server_notifications_{digits}.log | 15 | 15 | 1500 | utf-16-le x15 | date,network x15 | sha256:2c509fc280f438aa | shared date/network boilerplate x15 |
|
||||
| service_log.txt | 5 | 100 | 181445 | utf-8-sig x5 | time,login-event x100 | sha256:096a96f507532516 | service login-state skeletons x35/x35/x30 |
|
||||
| stats_log.txt | 5 | 100 | 769140 | utf-16-le x5 | none x100 | sha256:3eed2125dbb41774 | encoded/stat payload skeletons x95/x5 |
|
||||
| vehicle_destruction_{digits}.log | 15 | 72 | 503544 | utf-16-le x15 | date,coordinates x57; date,network x15 | sha256:af9b5eb7bece3c6d | vehicle coordinate event skeletons x18/x15; shared date/network boilerplate x15 |
|
||||
| violations_{digits}.log | 12 | 12 | 1200 | utf-16-le x12 | date,network x12 | sha256:2c509fc280f438aa | shared date/network boilerplate x12 |
|
||||
|
||||
Additional runtime/backup logs (`SCUM.log`, backup SCUM logs, `UE{n}SS.log`, `configstore_log.txt`, `systemmanager.txt`, and empty dispatch test stdout/stderr logs) were observed as separate service/runtime families. Their sampled content either did not decode into useful event skeletons under the bounded text decoder or contained no sampled lines; they are not candidate gameplay/event parsers without a separate parser contract.
|
||||
|
||||
## Log conclusions
|
||||
|
||||
- `login_{digits}.log` remains the authenticated player-session parser source because it has login/logout, coordinate, date, and network-shaped content markers matching the existing sanitized fixtures.
|
||||
- `gameplay_{digits}.log` and `vehicle_destruction_{digits}.log` contain coordinate-shaped events, but they are separate parser candidates and must not be conflated with login/session events.
|
||||
- Many gameplay logs start with a repeated date/network boilerplate shape, so parsers must strip or reject network-shaped tokens before persistence or logical event fingerprinting.
|
||||
- Service/runtime logs (`connection_log*.txt`, `service_log.txt`, `SCUM.log`, stats/config/system logs) are structurally distinct from gameplay event files and require separate declarations if used.
|
||||
-173
@@ -1,173 +0,0 @@
|
||||
# SCUM current-service DB schema baseline (redacted compact)
|
||||
|
||||
- diagnostic: scum-current-service-db-schema-compact-v1
|
||||
- observed_utc: 2026-08-13T03:26:25Z
|
||||
- db_path_sha256: 7c3880848ad9c049eeb66769a2b4460fa2c56009d0c022a13362aba8686efbd7
|
||||
- file_size_bytes: 80846848
|
||||
- mtime_unix: 1786591584
|
||||
- table_count: 161
|
||||
- redaction: no raw rows, SQL text, XML, host paths, credentials, sockets, IPs, or player identities
|
||||
|
||||
| table | rows | columns | primary_key_columns | foreign_key_count | index_count |
|
||||
|---|---:|---|---|---:|---:|
|
||||
| abandoned_bunker | 15 | id INTEGER PK NN; user_profile_id INTEGER; map_id INTEGER NN; location_x INTEGER NN; location_y INTEGER NN; is_day INTEGER NN; time_since_previous_activation_end REAL NN; time_until_activation_start REAL NN; keycard_override_activation_start REAL | id | 2 | 1 |
|
||||
| abandoned_bunker_alarmed_room | 0 | room_name TEXT NN; bunker_id INTEGER NN; alarm_time_remaining REAL NN | - | 1 | 1 |
|
||||
| abandoned_bunker_bcu_terminal | 4 | room_name TEXT NN; bunker_id INTEGER NN; last_download_time REAL NN | - | 1 | 2 |
|
||||
| abandoned_bunker_mesh_instance_bound_to_activation | 0 | abandoned_bunker_id INTEGER PK NN; mesh_instance_name TEXT PK NN; examine_time REAL NN | abandoned_bunker_id, mesh_instance_name | 1 | 1 |
|
||||
| abandoned_bunker_powered_room | 0 | room_name TEXT NN; bunker_id INTEGER NN | - | 1 | 1 |
|
||||
| abandoned_bunker_switchboard_fuse | 0 | room_name TEXT NN; bunker_id INTEGER NN; fuse_id INTEGER NN | - | 2 | 2 |
|
||||
| active_quest | 0 | id INTEGER PK NN; user_profile_id INTEGER; map_id INTEGER NN; quest_data_asset_path TEXT NN; auto_complete BOOLEAN NN; completion_deadline REAL NN; sector TEXT NN; quest_giver_type INTEGER NN; rewards_index INTEGER NN | id | 3 | 1 |
|
||||
| active_task | 1 | id INTEGER PK NN; user_profile_id INTEGER; map_id INTEGER NN; available_task_id INTEGER NN | id | 4 | 2 |
|
||||
| ammunition_data | 1673 | id INTEGER PK NN; ammunition_item_entity_class TEXT NN; health REAL NN; max_health REAL NN; default_max_health REAL NN; state INTEGER NN; entity_id INTEGER | id | 1 | 1 |
|
||||
| ammunition_item_entity | 2754 | entity_id INTEGER PK NN | entity_id | 1 | 0 |
|
||||
| animal_bait_feeder_item_entity | 67 | entity_id INTEGER PK NN; state INTEGER | entity_id | 1 | 0 |
|
||||
| available_task | 1 | id INTEGER PK NN; user_profile_id INTEGER; map_id INTEGER NN; task_data_asset_path TEXT NN; was_ever_completed BOOLEAN NN | id | 2 | 1 |
|
||||
| bank_account_registry | 73 | id INTEGER PK NN; map_id INTEGER; user_profile_id INTEGER; account_owner_user_profile_id INTEGER NN; bank_account_number INTEGER NN; save_timestamp INTEGER; used_digital_deluxe_privileges BOOLEAN | id | 3 | 3 |
|
||||
| bank_account_registry_cards | 92 | id INTEGER PK NN; map_id INTEGER; user_profile_id INTEGER; bank_account_id INTEGER NN; card_type TEXT; pin_number INTEGER; wrong_pins_remaining INTEGER; free_renewals_remaining INTEGER; daily_withdraw_amount_remaining INTEGER; daily_deposit_amount_remaining INTEGER; card_entity_id INTEGER | id | 4 | 3 |
|
||||
| bank_account_registry_currencies | 146 | id INTEGER PK NN; map_id INTEGER; user_profile_id INTEGER; bank_account_id INTEGER NN; currency_type INTEGER NN; account_balance INTEGER | id | 3 | 2 |
|
||||
| bank_general_data | 1 | id INTEGER PK NN; map_id INTEGER; user_profile_id INTEGER; last_generated_final_set_of_bank_account_numbers INTEGER; last_withdrawn_amount_reset_timestamp INTEGER | id | 2 | 1 |
|
||||
| base | 5 | id INTEGER PK; location_x REAL; location_y REAL; size_x NUMERIC; size_y REAL; name TEXT; map_id INTEGER; user_profile_id INTEGER; owner_user_profile_id INTEGER; is_owned_by_player INTEGER; bounds_min_x REAL; bounds_min_y REAL; bounds_max_x REAL; bounds_max_y REAL | id | 2 | 1 |
|
||||
| base_element | 1533 | element_id INTEGER PK; base_id INTEGER NN; location_x REAL; location_y REAL; location_z REAL; rotation_pitch REAL; rotation_yaw REAL; rotation_roll REAL; scale_x REAL; scale_y REAL; scale_z REAL; asset TEXT; element_health NUMERIC; owner_profile_id INTEGER; quality REAL; creator_prisoner_id INTEGER | element_id | 2 | 2 |
|
||||
| base_element_coloring | 7 | element_id INTEGER PK; element_part_index INTEGER PK NN; element_color_index INTEGER NN; element_pattern_index INTEGER NN | element_id, element_part_index | 1 | 1 |
|
||||
| base_element_flag | 5 | element_id INTEGER; overtake_end_time INTEGER; overtaker_user_profile_id INTEGER; expanded_elements INTEGER | - | 2 | 2 |
|
||||
| base_element_item | 6 | element_id INTEGER PK; item_entity_id INTEGER PK | element_id, item_entity_id | 2 | 2 |
|
||||
| base_element_shelter_map | 1 | element_id INTEGER PK NN; shelter_id INTEGER PK NN | element_id, shelter_id | 2 | 2 |
|
||||
| base_raid_protection | 0 | id INTEGER PK NN; manager_id INTEGER NN; base_flag_id INTEGER; data BLOB | id | 1 | 1 |
|
||||
| base_raid_protection_manager | 1 | id INTEGER PK NN; map_id INTEGER NN; user_profile_id INTEGER; protection_type TINYINT | id | 2 | 1 |
|
||||
| bcu_lock_registry | 0 | map_id INTEGER NN; user_profile_id INTEGER; server_user_profile_id INTEGER PK NN; bcu_lock_entity_id INTEGER NN; flag_element_id INTEGER NN | server_user_profile_id | 4 | 2 |
|
||||
| blocked_users | 0 | local_user_id TEXT NN; target_net_id TEXT NN | - | 1 | 1 |
|
||||
| chest_acquisition | 0 | base_id INTEGER PK NN; entity_id INTEGER PK NN; acquisition_start_time REAL NN; owning_user_profile_id INTEGER | base_id, entity_id | 2 | 2 |
|
||||
| continuous_usage_entity_component | 21 | entity_component_id INTEGER PK NN; amount FLOAT | entity_component_id | 1 | 0 |
|
||||
| cooking_instance | 1099 | id INTEGER PK NN; manager_id INTEGER NN; utility_type TEXT; bound_utility INTEGER; bound_recipe INTEGER; ingredients BLOB; temperature REAL; softPeakTemperature REAL; progress REAL; cook_quality INTEGER; owner_utility INTEGER; tag_index INTEGER; internal_coords BLOB; packed_locations BLOB; radius REAL; finished BOOLEAN; can_bound_other BOOLEAN; bounded_to INTEGER; cooked_time REAL; exhaustion_bonus REAL | id | 1 | 2 |
|
||||
| cooking_manager | 3 | id INTEGER PK NN; map_id INTEGER NN; user_profile_id INTEGER; snapped_keys BLOB; snapped_values BLOB | id | 2 | 1 |
|
||||
| custom_zone_configuration | 2 | id INTEGER PK NN; map_id INTEGER PK NN; name CHAR(100); color_red REAL; color_green REAL; color_blue REAL; handling_methods BIGINT; settings INTEGER | id, map_id | 1 | 2 |
|
||||
| custom_zone_configuration_damage_handling_methods | 32 | id INTEGER PK NN; custom_zone_configuration_id INTEGER NN; map_id INTEGER NN; damage_actor_type INTEGER; damage_handling_methods INTEGER | id | 2 | 0 |
|
||||
| custom_zone_region | 4 | id INTEGER PK NN; map_id INTEGER PK NN; name CHAR(100); location_x REAL; location_y REAL; size_x REAL; size_y REAL; configuration_index TINYINT; default_region_name CHAR(50); default_region_state TINYINT | id, map_id | 1 | 2 |
|
||||
| db_info | 1 | last_vacuum_time TEXT; next_free_id INTEGER | - | 0 | 0 |
|
||||
| discrete_amount_entity_component | 13829 | entity_component_id INTEGER PK NN; quantity INTEGER NN | entity_component_id | 1 | 0 |
|
||||
| dog_tag_item_entity | 0 | entity_id INTEGER PK NN; victim_user_profile_id INTEGER; victim_name TEXT; victim_fame_points INTEGER | entity_id | 2 | 1 |
|
||||
| door_locking_registry_data | 33 | asset TEXT PK NN; count INTEGER NN; type INTEGER PK NN; map_id INTEGER PK NN; user_profile_id INTEGER PK | asset, type, map_id, user_profile_id | 2 | 1 |
|
||||
| economy | 1 | id INTEGER PK NN; map_id INTEGER; user_profile_id INTEGER; save_timestamp INTEGER; time_since_last_economy_reset INTEGER NN | id | 2 | 1 |
|
||||
| economy_outpost_gold | 4 | id INTEGER PK NN; map_id INTEGER; user_profile_id INTEGER; outpost_id INTEGER NN; gold_buying_capability_funds INTEGER; gold_selling_capability_funds INTEGER; gold_selling_capability_funds_restock_amount REAL | id | 3 | 2 |
|
||||
| economy_outposts | 4 | id INTEGER PK NN; map_id INTEGER; user_profile_id INTEGER; outpost_runtime_id TEXT; buying_capability REAL; outpost_bank_funds REAL; economy_reset_elapsed_time REAL; prices_randomization_elapsed_time REAL; save_timestamp INTEGER; price_delta_seed REAL; tradeable_rotation_elapsed_time REAL | id | 2 | 1 |
|
||||
| economy_special_deals | 85 | id INTEGER PK NN; map_id INTEGER NN; user_profile_id INTEGER NN; sector TEXT NN; tradeable_asset TEXT NN; base_purchase_price INTEGER; amount_in_store INTEGER; override_purchase_ability BOOLEAN; can_be_purchased_by_player BOOLEAN; required_fame_points INTEGER; trader TEXT NN | id | 2 | 1 |
|
||||
| economy_tradeables_info | 12453 | id INTEGER PK NN; map_id INTEGER; user_profile_id INTEGER; trader_id INTEGER NN; tradeable_asset TEXT; amount_in_store INTEGER NN; restock_amount REAL; is_omitted_from_current_rotation BOOLEAN; partial_amount_in_store REAL | id | 3 | 2 |
|
||||
| economy_traders | 36 | id INTEGER PK NN; map_id INTEGER; user_profile_id INTEGER; trader_runtime_id TEXT; available_funds INTEGER | id | 2 | 1 |
|
||||
| elevated_users | 4 | user_id TEXT PK NN | user_id | 0 | 1 |
|
||||
| entity | 63379 | id INTEGER PK NN; entity_system_id INTEGER NN; class TEXT NN; owning_entity_id INTEGER; parent_entity_id INTEGER; location_x REAL NN; location_y REAL NN; location_z REAL NN; rotation_x REAL NN; rotation_y REAL NN; rotation_z REAL NN; scale_x REAL NN; scale_y REAL NN; scale_z REAL NN; flags INTEGER NN; data BLOB; reason TEXT | id | 3 | 3 |
|
||||
| entity_character_inventory_component | 72 | entity_component_id INTEGER NN; last_entity_on_lshoulder INTEGER; last_entity_on_rshoulder INTEGER | - | 3 | 0 |
|
||||
| entity_component | 62174 | id INTEGER PK NN; entity_id INTEGER NN; name TEXT NN; class TEXT NN; flags INTEGER NN; data BLOB | id | 1 | 1 |
|
||||
| entity_component_to_delete_on_startup | 0 | entity_component_id INTEGER PK NN | entity_component_id | 1 | 0 |
|
||||
| entity_decay_manager_entry | 41286 | entity_id INTEGER PK NN; timestamp REAL NN; damage_per_day REAL NN | entity_id | 1 | 0 |
|
||||
| entity_inventory_component_entry | 2034 | entity_component_id INTEGER PK NN; entity_id INTEGER PK NN; data INTEGER NN | entity_component_id, entity_id | 2 | 2 |
|
||||
| entity_system | 1 | id INTEGER PK NN; map_id INTEGER NN; user_profile_id INTEGER; timestamp REAL NN | id | 2 | 1 |
|
||||
| entity_to_delete_on_startup | 6 | entity_id INTEGER PK NN | entity_id | 1 | 0 |
|
||||
| event | 0 | id INTEGER PK NN | id | 0 | 0 |
|
||||
| event_participant | 0 | event_id INTEGER PK NN; user_profile_id INTEGER PK NN | event_id, user_profile_id | 2 | 2 |
|
||||
| event_rankings_cached | 73 | user_id TEXT NN; user_profile_id INTEGER PK NN; name TEXT NN; is_banned INTEGER NN; fame_points REAL NN; score REAL NN; enemy_kills INTEGER NN; team_kills INTEGER NN; deaths INTEGER NN; suicides INTEGER NN; assists INTEGER NN; headshots INTEGER NN; ctf_pickups INTEGER NN; ctf_captures INTEGER NN; ctf_returns INTEGER NN; dm_longest_headshot REAL NN; dm_melee_kills INTEGER NN; dm_longest_life REAL NN; dz_activations INTEGER NN | user_profile_id | 2 | 1 |
|
||||
| event_round | 0 | id INTEGER PK NN; event_id INTEGER NN; start_time TEXT; end_time TEXT | id | 1 | 1 |
|
||||
| event_round_stats | 0 | round_id INTEGER PK NN; user_profile_id INTEGER PK NN; score REAL; enemy_kills INTEGER; team_kills INTEGER; deaths INTEGER; suicides INTEGER; assists INTEGER; headshots INTEGER | round_id, user_profile_id | 2 | 2 |
|
||||
| event_round_stats_ctf | 0 | round_id INTEGER PK NN; user_profile_id INTEGER PK NN; pickups INTEGER; captures INTEGER; returns INTEGER | round_id, user_profile_id | 2 | 2 |
|
||||
| event_round_stats_dm | 0 | round_id INTEGER PK NN; user_profile_id INTEGER PK NN; longest_headshot REAL; melee_kills INTEGER; longest_life REAL | round_id, user_profile_id | 2 | 2 |
|
||||
| event_round_stats_dz | 0 | round_id INTEGER PK NN; user_profile_id INTEGER PK NN; activations INTEGER | round_id, user_profile_id | 2 | 2 |
|
||||
| events_stats | 73 | user_profile_id INTEGER PK NN; events_won INTEGER; events_lost INTEGER; enemy_kills INTEGER; deaths INTEGER; ctf_captures INTEGER; team_kills INTEGER | user_profile_id | 1 | 0 |
|
||||
| expirable_entity_component | 71 | entity_component_id INTEGER PK NN; start_play_time REAL; duration REAL | entity_component_id | 1 | 0 |
|
||||
| expirable_vehicle_spawner_info | 0 | map_id INTEGER PK; user_profile_id INTEGER PK; spawner_name TEXT PK; num_spawners_created INTEGER | map_id, user_profile_id, spawner_name | 2 | 1 |
|
||||
| finished_timed_gift_spawner | 2 | map_id INTEGER PK; user_profile_id INTEGER PK; spawn_time INTEGER PK | map_id, user_profile_id, spawn_time | 2 | 1 |
|
||||
| fishing_stats | 73 | user_profile_id INTEGER PK NN; fish_caught INTEGER; fish_kept INTEGER; fish_released INTEGER; lines_broken INTEGER; heaviest_fish_caught REAL; longest_fish_caught REAL; bass_caught INTEGER; catfish_caught INTEGER; pike_caught INTEGER; carp_caught INTEGER; amur_caught INTEGER; bleak_caught INTEGER; chub_caught INTEGER; ruffe_caught INTEGER; prussian_carp_caught INTEGER; crucian_carp_caught INTEGER; sardine_caught INTEGER; dentex_caught INTEGER; orata_caught INTEGER; tuna_caught INTEGER | user_profile_id | 1 | 0 |
|
||||
| food_item_entity | 5693 | entity_id INTEGER PK NN; damage_over_time_outside_refrigerator REAL; is_opened INTEGER | entity_id | 1 | 0 |
|
||||
| garden | 2 | id INTEGER PK NN; garden_manager_id INTEGER NN; owner_user_profile_id INTEGER; owner_skill_level INTEGER; num_slots_x INTEGER; num_slots_y INTEGER; location_x REAL; location_y REAL; location_z REAL; rotation_r REAL; rotation_p REAL; rotation_y REAL; crafted_age REAL; has_greenhouse BOOL; pest_disease_check_timer REAL; health REAL NN | id | 2 | 2 |
|
||||
| garden_manager | 1 | id INTEGER PK NN; user_profile_id INTEGER; map_id INTEGER NN; sim_data_sun_irradiance REAL; sim_data_rain_average REAL; sim_data_time_range REAL | id | 2 | 1 |
|
||||
| garden_manager_height_relative_sim_data | 16 | id INTEGER PK NN; garden_manager_id INTEGER NN; height REAL; temperature REAL; water_evaporation REAL | id | 1 | 1 |
|
||||
| garden_slot | 110 | id INTEGER PK NN; garden_id INTEGER NN; planter_user_profile_id INTEGER; planter_skill_level INTEGER; slot_position_x INTEGER; slot_position_y INTEGER; species TEXT; stage INTEGER; growth_percentage REAL; water REAL; organic_fertilizer BOOL; industrial_fertilizer BOOL; weed_intenstiy REAL; pesticide_timer REAL; fungicide_timer REAL; weed_repellent_timer REAL; health REAL; dehidration_death_timer REAL; freezing_death_timer REAL; pest_death_timer REAL; disease_death_timer REAL; old_age_death_timer REAL; weed_check_timer REAL; dead BOOL | id | 2 | 2 |
|
||||
| garden_slot_pest | 330 | id INTEGER PK NN; garden_slot_id INTEGER NN; pest_species TEXT; pest_intensity REAL; disease_species TEXT; disease_intensity REAL | id | 1 | 1 |
|
||||
| global_aquatic_life_manager | 6 | id INTEGER PK NN; user_profile_id INTEGER; map_id INTEGER NN | id | 2 | 0 |
|
||||
| global_aquatic_life_manager_special_volume_types | 6 | id INTEGER PK NN; global_aquatic_life_manager_id INTEGER_NOT_NULL; volume_type TEXT; activation_timestamp REAL | id | 1 | 0 |
|
||||
| global_aquatic_life_manager_special_volumes | 54 | id INTEGER PK NN; global_aquatic_life_manager_special_volume_type_id INTEGER_NOT_NULL; persistent_id TEXT | id | 1 | 0 |
|
||||
| global_encounter_manager | 1 | id INTEGER PK NN; map_id INTEGER NN; user_profile_id INTEGER; max_group_id INTEGER | id | 2 | 1 |
|
||||
| global_encounter_manager_bb_data | 0 | id INTEGER PK NN; global_encounter_manager_id INTEGER NN; base_id INTEGER NN; at_least_one_base_member_present_time REAL; encounter_cooldown REAL; base_lifetime REAL | id | 1 | 2 |
|
||||
| global_encounter_manager_bb_data_flags | 0 | id INTEGER PK NN; global_encounter_manager_id INTEGER NN; global_encounter_manager_bb_data_id INTEGER NN; flag_id INTEGER NN | id | 2 | 2 |
|
||||
| global_encounter_manager_rogue_characters | 0 | id INTEGER PK NN; global_encounter_manager_id INTEGER NN; character_class STRING; character_location_x REAL; character_location_y REAL; character_location_z REAL; character_rotation_yaw REAL; character_data BLOB; preset_type TEXT; despawn_lifetime REAL; despawn_lifetime_remaining REAL | id | 1 | 1 |
|
||||
| global_encounter_manager_visited_points | 0 | id INTEGER PK NN; global_encounter_manager_id INTEGER NN; location_x REAL; location_y REAL; location_z REAL; xy_size REAL; z_size REAL; stay_alive_timestamp REAL; remove_time REAL; is_subzone_point BOOLEAN | id | 1 | 1 |
|
||||
| global_encounter_manager_zone_cooldown | 0 | id INTEGER PK NN; global_encounter_manager_id INTEGER NN; zone_location_x REAL; zone_location_y REAL; zone_location_z REAL; sub_zone_index INTEGER; cooldown REAL | id | 1 | 1 |
|
||||
| global_radiation_data | 1 | map_id INTEGER PK NN; user_profile_id INTEGER PK; timestamp INTEGER NN | map_id, user_profile_id | 2 | 1 |
|
||||
| gold_price_master_multipliers | 0 | id INTEGER PK NN; date INTEGER; multiplier REAL | id | 0 | 0 |
|
||||
| heat_source | 4769 | id INTEGER PK NN; map_id INTEGER; user_profile_id INTEGER; location_x REAL; location_y REAL; location_z REAL; temperature REAL; burning_speed REAL; distance_scale REAL; inner_radius REAL; outer_radius REAL; fuel_capacity REAL; temperature_curve_path TEXT; fuel BLOB; uses_fuel_simulation BOOL NN; operating_temperature REAL | id | 2 | 1 |
|
||||
| ignitable_component_entry | 4 | entity_component_id INTEGER PK NN; was_ever_ignited INTEGER | entity_component_id | 1 | 0 |
|
||||
| item_container_upgrades | 0 | id INTEGER PK NN; item_container_id INTEGER NN; asset TEXT; num_tries INTEGER | id | 1 | 1 |
|
||||
| item_containers | 0 | id INTEGER PK NN; map_id INTEGER NN; user_profile_id INTEGER; runtime_id TEXT NN; lock_reset_time INTEGER | id | 2 | 1 |
|
||||
| item_entity | 62853 | entity_id INTEGER PK NN; health REAL; max_health REAL; radiation_amount REAL; xml TEXT; weight REAL; water_weight REAL; external_weight REAL; total_weight REAL; max_uses INTEGER; flags INTEGER; body_damage_data BLOB | entity_id | 1 | 0 |
|
||||
| item_entity_spawner | 136794 | id INTEGER PK NN; name TEXT NN; entity_system_id INTEGER NN; evaluation_time REAL NN; num_evaluations INTEGER | id | 1 | 1 |
|
||||
| item_entity_spawner_cooldown | 17 | item_entity_spawner_id INTEGER PK NN; key INTEGER PK NN; value REAL NN | item_entity_spawner_id, key | 1 | 2 |
|
||||
| item_entity_spawner_entry | 155893 | item_entity_spawner_id INTEGER PK NN; idx INTEGER PK NN; entity_id INTEGER | item_entity_spawner_id, idx | 2 | 3 |
|
||||
| item_radiation_exposure_data | 0 | item_id_1 INTEGER PK NN; item_id_2 INTEGER PK NN; map_id INTEGER PK NN; user_profile_id INTEGER PK; radiation_amount REAL NN; pending_radiation_amount REAL NN; modified_timestamp INTEGER NN | item_id_1, item_id_2, map_id, user_profile_id | 2 | 1 |
|
||||
| item_spawner_evaluation_result_entity_component | 20522 | entity_component_id INTEGER PK NN; usage_ratio REAL NN; health_ratio REAL NN | entity_component_id | 1 | 0 |
|
||||
| item_spawner_evaluation_result_entity_component_post_spawn_action | 30782 | entity_component_id INTEGER PK NN; idx INTEGER PK NN; action TEXT NN | entity_component_id, idx | 1 | 1 |
|
||||
| item_spawning_cooldown_groups | 18 | entity_system_id INTEGER NN; idx INTEGER NN; cooldown_min INTEGER NN; cooldown_max INTEGER NN | - | 1 | 1 |
|
||||
| key_card_item_entity | 4 | entity_id INTEGER PK NN; spawn_reason TEXT | entity_id | 1 | 0 |
|
||||
| killbox | 12 | name TEXT PK NN; map_id INTEGER PK NN; user_profile_id INTEGER PK; is_active BOOL; is_finale BOOL; remaining_time REAL; rooms_difficulty_packed INTEGER; rooms_state_packed INTEGER | name, map_id, user_profile_id | 2 | 1 |
|
||||
| local_chat_history | 0 | chat_line TEXT NN | - | 0 | 0 |
|
||||
| map | 1 | id INTEGER PK NN; name TEXT | id | 0 | 0 |
|
||||
| muted_users | 0 | user_id_hashed TEXT PK; user_profile_name TEXT | user_id_hashed | 0 | 1 |
|
||||
| notification | 0 | id INTEGER PK NN; user_profile_id INTEGER NN; data BLOB; timestamp INTEGER | id | 1 | 1 |
|
||||
| penalty_squad_leave_info | 0 | user_id TEXT PK; squadmates_left INTEGER NN | user_id | 1 | 1 |
|
||||
| placeable | 16 | id INTEGER PK NN; prisoner_id INTEGER NN; map_id INTEGER NN; asset TEXT; location_x REAL; location_y REAL; location_z REAL; rotation_pitch REAL; rotation_yaw REAL; rotation_roll REAL; placement_location_x REAL; placement_location_y REAL; placement_location_z REAL; process BLOB | id | 2 | 1 |
|
||||
| placeable_basebuilding | 16 | placeable_id INTEGER NN; first_point_location_x REAL; first_point_location_y REAL; first_point_location_z REAL; state_flags INTEGER; first_point_rotation_pitch REAL; first_point_rotation_yaw REAL; first_point_rotation_roll REAL | - | 1 | 1 |
|
||||
| placeable_garden | 0 | id INTEGER PK NN; placeable_id INTEGER NN; x_min INTEGER; y_min INTEGER; x_max INTEGER; y_max INTEGER | id | 1 | 1 |
|
||||
| placeable_upgrade | 0 | placeable_id INTEGER NN; element_base_id INTEGER NN; element_id INTEGER NN; element_location_x REAL; element_location_y REAL; element_location_z REAL | - | 1 | 1 |
|
||||
| prisoner | 72 | id INTEGER PK NN; user_profile_id INTEGER; is_alive INTEGER; time_of_death TEXT; team_index INTEGER; appearance_index INTEGER; gender INTEGER; head_tattoo_index INTEGER; body_tattoo_index INTEGER; stance INTEGER; melee_target_selection_mode INTEGER; head_water_weight REAL; upper_body_water_weight REAL; lower_body_water_weight REAL; feet_water_weight REAL; penis_size REAL; breast_size INTEGER; time_of_revive TEXT; body_simulation BLOB; should_play_intro_cinematic BOOLEAN; has_open_parachute BOOLEAN; eligible_for_free_plastic_surgery BOOLEAN; last_character_dlcs_owned BOOLEAN; age TINYINT; appearance_hair_style_index TINYINT; appearance_face_type_index TINYINT; appearance_skin_tone_index TINYINT; appearance_hair_color_index TINYINT; appearance_eye_color_index TINYINT; appearance_iris_type_index TINYINT; appearance_eye_makeup_metalness TINYINT; appearance_eye_makeup_intensity TINYINT; appearance_lipstick_roughness TINYINT; appearance_lipstick_intensity TINYINT; appearance_eyeshadow_color_r TINYINT; appearance_eyeshadow_color_g TINYINT; appearance_eyeshadow_color_b TINYINT; appearance_eyeliner_color_r TINYINT; appearance_eyeliner_color_g TINYINT; appearance_eyeliner_color_b TINYINT; appearance_lipstick_color_r TINYINT; appearance_lipstick_color_g TINYINT; appearance_lipstick_color_b TINYINT; awarded_character_dlc_plastic_surgeries BOOLEAN; should_spawn_default_equipment INTEGER; last_save_time INTEGER; is_in_bed BOOLEAN; appearance_moustache_style_index INTEGER; appearance_beard_style_index INTEGER; appearance_facial_hair_color_index INTEGER; appearance_has_body_hair BOOLEAN; appearance_tattoo_indexes BLOB | id | 1 | 2 |
|
||||
| prisoner_bondage | 0 | prisoner_id INTEGER PK NN; body_part INTEGER PK NN; tightness REAL; item_entity_id INTEGER NN | prisoner_id, body_part | 2 | 2 |
|
||||
| prisoner_consumed_items | 205 | prisoner_id INTEGER PK NN; item_name TEXT PK NN; last_consumed_time INTEGER; consumed_amount REAL | prisoner_id, item_name | 1 | 1 |
|
||||
| prisoner_disease_immunities | 26 | prisoner_id INTEGER PK NN; disease_name TEXT PK NN; last_recovered_play_time INTEGER | prisoner_id, disease_name | 1 | 1 |
|
||||
| prisoner_entity | 72 | entity_id INTEGER PK NN; prisoner_id INTEGER | entity_id | 2 | 1 |
|
||||
| prisoner_inventory_quick_access_slot | 31 | prisoner_entity_id INTEGER PK NN; slot_index INTEGER PK NN; item_entity_setup TEXT NN; item_entity_id INTEGER; is_in_throwing_mode INTEGER NN | prisoner_entity_id, slot_index | 2 | 2 |
|
||||
| prisoner_prison_wallet | 17 | prisoner_entity_id INTEGER PK NN; is_deluxe_item_pending INTEGER NN | prisoner_entity_id | 1 | 0 |
|
||||
| prisoner_respawn_info | 70 | prisoner_id INTEGER PK; penalty_random INTEGER; penalty_sector INTEGER; penalty_shelter INTEGER; penalty_squad INTEGER; last_use_random TEXT; last_use_sector TEXT; last_use_shelter TEXT; last_use_squad TEXT; commit_suicide_penalty INTEGER; commit_suicide_last_use TEXT; commit_suicide_cooldown_left FLOAT | prisoner_id | 1 | 0 |
|
||||
| prisoner_skill | 1656 | prisoner_id INTEGER NN; name TEXT; level INTEGER; experience REAL; xml TEXT | - | 1 | 1 |
|
||||
| prisoner_spawn_location | 78 | prisoner_id INTEGER PK NN; map_id INTEGER PK NN; type INTEGER PK NN; shelter_id INTEGER; location_x REAL; location_y REAL; location_z REAL; rotation_pitch REAL; rotation_yaw REAL; rotation_roll REAL; velocity_x REAL; velocity_y REAL; velocity_z REAL | prisoner_id, map_id, type | 3 | 1 |
|
||||
| prisoner_stats_tracking_info | 1 | prisoner_id INTEGER PK NN; has_ever_maxed_out_core_attributes INTEGER | prisoner_id | 1 | 0 |
|
||||
| prisoner_vehicle_mountee_info | 0 | prisoner_id INTEGER PK NN; vehicle_entity_id INTEGER PK NN; mount_slot_index INTEGER | prisoner_id, vehicle_entity_id | 2 | 2 |
|
||||
| probation_squad_leave_info | 0 | user_id TEXT PK; start_timestamp INTEGER NN; squadmates_left INTEGER NN | user_id | 1 | 1 |
|
||||
| quest_cycle_stats | 0 | id INTEGER PK NN; user_profile_id INTEGER NN; map_id INTEGER NN; associated_npc TEXT; sector TEXT; num_completed_quests INTEGER NN | id | 2 | 1 |
|
||||
| quest_in_pool | 0 | id INTEGER PK NN; quest_pool_id INTEGER NN; quest_mapping_key TEXT NN; sector TEXT; rewards_index INTEGER NN; random_seed INTEGER NN | id | 1 | 1 |
|
||||
| quest_lifetime_stats | 63 | id INTEGER PK NN; user_profile_id INTEGER NN; map_id INTEGER NN; associated_npc TEXT; tier INTEGER NN; sector TEXT NN; total_num_completed_quests INTEGER NN | id | 2 | 1 |
|
||||
| quest_pool | 0 | id INTEGER PK NN; user_profile_id INTEGER; map_id INTEGER NN; quest_giver_hash INTEGER NN; quest_giver_bound_user_id INTEGER; quest_giver_type TEXT NN | id | 2 | 2 |
|
||||
| quest_refresh | 1 | id INTEGER PK NN; user_profile_id INTEGER; map_id INTEGER NN; next_reset_timestamp REAL NN | id | 2 | 1 |
|
||||
| quest_unlocked_tag | 30 | id INTEGER PK NN; user_profile_id INTEGER NN; map_id INTEGER NN; tag TEXT NN | id | 2 | 1 |
|
||||
| rain_collector | 5637 | id INTEGER PK NN; map_id INTEGER PK NN; user_profile_id INTEGER PK; amount REAL; fill_rate REAL; max_amount REAL | id, map_id, user_profile_id | 2 | 1 |
|
||||
| remote_sensor_registry_key_codes_to_unpair | 1 | map_id INTEGER NN; user_profile_id INTEGER; key_code TEXT NN | - | 2 | 1 |
|
||||
| replenishable_resource | 584 | id INTEGER PK NN; map_id INTEGER NN; user_profile_id INTEGER; desc BLOB; amount REAL; timer REAL; save_time INTEGER | id | 2 | 1 |
|
||||
| restorable_mesh_instance | 0 | id INTEGER PK NN; map_id INTEGER NN; user_profile_id INTEGER; component_name TEXT; packed_location INTEGER; location_x REAL; location_y REAL; location_z REAL; rotation_x REAL; rotation_y REAL; rotation_z REAL; rotation_w REAL; scale_x REAL; scale_y REAL; scale_z REAL; restore_interval REAL; restore_timer REAL; save_time INTEGER | id | 2 | 1 |
|
||||
| sentries | 0 | id INTEGER PK NN; user_profile_id INTEGER; map_id INTEGER; sentry_location_x REAL; sentry_location_y REAL; sentry_location_z REAL; sentry_rotation_yaw REAL; sentry_health REAL; sentry_longrangeweapon_ammo INTEGER | id | 2 | 1 |
|
||||
| sentry_ai_controllers | 0 | id INTEGER PK NN; user_profile_id INTEGER; map_id INTEGER; sentry_id INTEGER; sentry_state INTEGER; player_last_known_location_x REAL; player_last_known_location_y REAL; player_last_known_location_z REAL; player_last_known_direction_x REAL; player_last_known_direction_y REAL; player_last_known_direction_z REAL; player_threat_level REAL; player_has_entered_hot_zone BOOL | id | 3 | 2 |
|
||||
| sentry_spawners | 0 | id INTEGER PK NN; user_profile_id INTEGER; map_id INTEGER; spawner_location_x REAL; spawner_location_y REAL; spawner_location_z REAL; sentry_id INTEGER | id | 3 | 2 |
|
||||
| server_settings | 1 | enable_item_cooldown_groups INTEGER NN; is_radiation_enabled INTEGER | - | 0 | 0 |
|
||||
| shelter | 817 | id INTEGER PK NN | id | 0 | 0 |
|
||||
| shown_dialogues | 0 | map_id INTEGER PK NN; user_profile_id INTEGER PK; dialogue_name TEXT PK NN | map_id, user_profile_id, dialogue_name | 2 | 1 |
|
||||
| shown_survival_tips | 0 | map_id INTEGER PK NN; user_profile_id INTEGER PK; survival_tip_name TEXT PK NN; seen_in_codex BOOLEAN | map_id, user_profile_id, survival_tip_name | 2 | 1 |
|
||||
| spawned_item_expiration_data | 23 | id INTEGER PK NN; map_id INTEGER NN; user_profile_id INTEGER; examiner_user_id TEXT NN; item_asset TEXT NN; item_expiration_timestamp INTEGER NN | id | 3 | 2 |
|
||||
| squad | 7 | id INTEGER PK NN; name TEXT; message TEXT; emblem INTEGER; information TEXT; score REAL; member_limit INTEGER; last_member_login_time TEXT; last_member_logout_time TEXT | id | 0 | 1 |
|
||||
| squad_member | 18 | id INTEGER PK NN; squad_id INTEGER NN; user_profile_id INTEGER NN; rank INTEGER | id | 2 | 2 |
|
||||
| stackable_component_entry | 5120 | entity_component_id INTEGER PK NN; foreign_classes_blob BLOB | entity_component_id | 1 | 0 |
|
||||
| survival_stats | 73 | user_profile_id INTEGER PK NN; highest_positive_fame_points REAL; doors_claimed INTEGER; animals_killed INTEGER; minutes_survived REAL; kills INTEGER; deaths INTEGER; locks_picked INTEGER; puppets_killed INTEGER; guns_crafted INTEGER; bullets_crafted INTEGER; arrows_crafted INTEGER; clothing_crafted INTEGER; longest_kill_distance REAL; melee_kills INTEGER; archery_kills INTEGER; players_knocked_out INTEGER; total_defecations INTEGER; total_urinations INTEGER; lights_fired INTEGER; containers_looted INTEGER; items_put_into_containers INTEGER; deaths_by_prisoners INTEGER; animals_skinned INTEGER; food_eaten REAL; distance_travelled_by_foot REAL; wounds_patched INTEGER; items_picked_up INTEGER; liquid_drank REAL; teeth_lost INTEGER; total_calories_intake INTEGER; shots_fired INTEGER; shots_hit INTEGER; headshots INTEGER; melee_weapon_swings INTEGER; melee_weapon_hits INTEGER; melee_weapons_crafted INTEGER; drone_kills INTEGER; sentry_kills INTEGER; prisoner_kills INTEGER; puppets_knocked_out INTEGER; diarrheas INTEGER; vomits INTEGER; distance_travelled_in_vehicle REAL; mushrooms_eaten INTEGER; highest_muscle_mass REAL; highest_fat REAL; heart_attacks INTEGER; overdose INTEGER; starvation INTEGER; highest_damage_taken REAL; highest_weight_carried REAL; lowest_negative_fame_points REAL; distance_travelled_swimming REAL; crows_killed INTEGER; seagulls_killed INTEGER; horses_killed INTEGER; boars_killed INTEGER; bears_killed INTEGER; goats_killed INTEGER; deers_killed INTEGER; chickens_killed INTEGER; rabbits_killed INTEGER; donkeys_killed INTEGER; times_mauled_by_bear INTEGER; longest_animal_kill_distance REAL; alcohol_drank INTEGER NN; foliage_cut INTEGER NN; distance_travel_by_boat REAL NN; distance_sailed REAL NN; times_caught_by_shark INTEGER NN; times_escaped_shark_bite INTEGER NN; wolves_killed INTEGER; last_fame_point_award_consecutive_days INTEGER; firearm_kills INTEGER; bare_handed_kills INTEGER | user_profile_id | 1 | 0 |
|
||||
| tracked_quest | 37 | id INTEGER PK NN; user_profile_id INTEGER NN; map_id INTEGER NN; quest_id INTEGER NN; type INTEGER NN | id | 3 | 2 |
|
||||
| tracking_data | 194 | id INTEGER PK NN; tracking_data_set_id INTEGER NN; data BLOB; version INTEGER; random_seed INTEGER NN | id | 1 | 1 |
|
||||
| tracking_data_set | 38 | id INTEGER PK NN; user_profile_id INTEGER; map_id INTEGER NN; sequence_index INTEGER | id | 2 | 1 |
|
||||
| user | 74 | id TEXT PK NN; name TEXT; provider TEXT; last_login_time TEXT; last_direct_connection_address TEXT; is_banned INTEGER; has_used_new_player_protection BOOLEAN; creation_time TEXT; id_type TEXT | id | 0 | 1 |
|
||||
| user_accepted_policies | 0 | user_id TEXT PK NN; policy_id TEXT PK NN; accepted_version INTEGER NN | user_id, policy_id | 1 | 1 |
|
||||
| user_favorite_server | 0 | user_id TEXT NN; name TEXT; host TEXT; port INTEGER | - | 1 | 1 |
|
||||
| user_profile | 73 | id INTEGER PK NN; user_id TEXT; template_xml TEXT; name TEXT; type INTEGER; authority_name TEXT; authority_ip TEXT; authority_response_port INTEGER; authority_gameplay_port INTEGER; authority_user_profile_id INTEGER; authority_auth_token TEXT; last_login_time TEXT; prisoner_id INTEGER; fame_points REAL; fake_name TEXT; last_logout_time TEXT; money_balance INTEGER; global_spam_protection_state BLOB; local_spam_protection_state BLOB; last_name_change TEXT; favorite_crafting_recipes BLOB; deluxe_version BLOB; play_time INTEGER; has_used_new_player_protection BOOLEAN; creation_time TEXT | id | 2 | 3 |
|
||||
| user_profiles_marked_for_deletion | 0 | id INTEGER PK NN; user_id INTEGER NN; user_profile_id INTEGER NN | id | 2 | 3 |
|
||||
| user_recent_server | 0 | user_id TEXT NN; name TEXT; host TEXT; port INTEGER | - | 1 | 1 |
|
||||
| vehicle_entity | 313 | entity_id INTEGER PK NN; item_container_entity_id INTEGER; data BLOB | entity_id | 2 | 1 |
|
||||
| vehicle_service | 0 | map_id INTEGER PK NN; user_profile_id INTEGER PK; vehicle_id INTEGER PK NN; initiator_id INTEGER; service_station_data BLOB | map_id, user_profile_id, vehicle_id | 3 | 2 |
|
||||
| vehicle_spawner | 313 | vehicle_entity_id INTEGER PK NN; vehicle_asset_id TEXT NN; vehicle_alias TEXT NN; vehicle_last_access_time INTEGER NN; is_vehicle_automatically_created BOOL NN; time_spent_in_forbidden_zone REAL NN; is_vehicle_functional BOOL NN | vehicle_entity_id | 1 | 0 |
|
||||
| virtualized_encounters | 318 | id INTEGER PK NN; global_encounter_manager_id INTEGER NN; virutalized_encounter_data BLOB; reset_time REAL | id | 1 | 1 |
|
||||
| virtualized_item | 3943 | item_entity_id INTEGER PK; expiration_time INTEGER; item_name TEXT; can_expire INTEGER; save_time INTEGER; bounds_size REAL; queued_visit_time INTEGER; item_user_data BLOB; owner_user_profile_id INTEGER | item_entity_id | 2 | 1 |
|
||||
| weapon_attachment_item_entity | 1212 | entity_id INTEGER PK NN | entity_id | 1 | 0 |
|
||||
| weapon_attachment_magazine_item_entity | 1060 | entity_id INTEGER PK NN | entity_id | 1 | 0 |
|
||||
| weapon_attachment_magazine_item_entity_ammo_data | 1613 | weapon_attachment_magazine_item_entity_id INTEGER PK NN; ammunition_data_id INTEGER PK NN | weapon_attachment_magazine_item_entity_id, ammunition_data_id | 2 | 2 |
|
||||
| weapon_item_entity | 432 | entity_id INTEGER PK NN | entity_id | 1 | 0 |
|
||||
| weapon_item_entity_internal_magazine_ammo_data | 34 | weapon_item_entity_id INTEGER PK NN; ammunition_data_id INTEGER PK NN | weapon_item_entity_id, ammunition_data_id | 2 | 2 |
|
||||
| weapon_item_entity_loaded_ammo_data | 26 | weapon_item_entity_id INTEGER PK NN; ammunition_data_id INTEGER PK NN | weapon_item_entity_id, ammunition_data_id | 2 | 2 |
|
||||
| weather_parameters | 1 | map_id INTEGER PK NN; user_profile_id INTEGER PK; time_of_day REAL; moon_rotation REAL; base_air_temperature REAL; water_temperature REAL; should_cumulonimbus_cause_fog INTEGER; fog_density REAL; data BLOB | map_id, user_profile_id | 2 | 1 |
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
# SCUM current-service log structure baseline (redacted)
|
||||
|
||||
- diagnostic: scum-current-service-log-structure-v4
|
||||
- observed_utc: 2026-08-13T03:24:34Z
|
||||
- root_strategy: running-process-adjacent plus common profile roots
|
||||
- roots_scanned: 9
|
||||
- log_file_count: 1200
|
||||
- redaction: no host paths, raw log lines, credentials, sockets, IPs, or player identities
|
||||
|
||||
## Directory fingerprints
|
||||
|
||||
| directory_sha256 | files | bytes | oldest_mtime | newest_mtime | filename_patterns |
|
||||
|---|---:|---:|---|---|---|
|
||||
| 8f7b4724b579a891c9208f9a67116925f66b4c4ed9de67024a76bc846ecbfad2 | 1136 | 23425208 | 2026-03-27T19:37:41Z | 2026-08-13T03:19:26Z | admin_{digits}.log x60; armor_absorption_{digits}.log x60; base_building_destruction_{digits}.log x60; chat_{digits}.log x60; chest_ownership_{digits}.log x60; economy_{digits}.log x60; event_kill_{digits}.log x60; famepoints_{digits}.log x60; gameplay_{digits}.log x60; kill_{digits}.log x60; login_{digits}.log x60; loot_{digits}.log x60; quests_{digits}.log x60; raid_protection_{digits}.log x60; sentry_{digits}.log x60; server_notifications_{digits}.log x60 |
|
||||
| 3f15fc215272bffbd13ec0381693a72c88bd2506542387b441ea5f26c9537d29 | 52 | 7002932 | 2025-08-27T04:07:52Z | 2026-08-13T00:18:24Z | connection_log_{n}.txt x16; connection_log_{n}.previous.txt x8; configstore_log.txt x4; connection_log.previous.txt x4; connection_log.txt x4; service_log.txt x4; stats_log.previous.txt x4; stats_log.txt x4; systemmanager.txt x4 |
|
||||
| 03976d194b46bbb5ec3e755c9d73dd4e3240ea209ebccf7f6d399281e590f867 | 6 | 13660622 | 2026-03-24T03:31:36Z | 2026-08-13T03:24:33Z | SCUM_{n}.log x4; SCUM.log x2 |
|
||||
| 74e7a2b6564768eab96dafeb19297a6b0c8400eeeff30fc88c5a03162898b5db | 4 | 24 | 2025-07-29T16:16:59Z | 2025-07-29T16:16:59Z | steam_appid.txt x4 |
|
||||
| 152191e5251dd1d77824d1ffe4726eca62e2a647205c36cf4ea153bb0b6b4e90 | 2 | 35815054 | 2025-11-21T21:37:44Z | 2025-11-21T21:37:44Z | SCUM.log x2 |
|
||||
|
||||
## Filename pattern inventory
|
||||
|
||||
| filename_pattern | files | bytes | oldest_mtime | newest_mtime | encodings_seen |
|
||||
|---|---:|---:|---|---|---|
|
||||
| SCUM.log | 4 | 48218920 | 2025-11-21T21:37:44Z | 2026-08-13T03:24:33Z | utf-8-sig x4 |
|
||||
| SCUM_{n}.log | 4 | 1256756 | 2026-03-24T03:31:36Z | 2026-05-29T11:51:50Z | utf-8-sig x4 |
|
||||
| admin_{digits}.log | 60 | 87840 | 2026-03-27T19:37:41Z | 2026-08-12T23:56:42Z | unknown x60 |
|
||||
| armor_absorption_{digits}.log | 60 | 6000 | 2026-03-27T19:37:41Z | 2026-08-12T01:18:15Z | unknown x60 |
|
||||
| base_building_destruction_{digits}.log | 60 | 19264 | 2026-03-27T19:37:41Z | 2026-08-12T01:18:15Z | unknown x60 |
|
||||
| chat_{digits}.log | 60 | 215608 | 2026-04-02T21:46:35Z | 2026-08-12T23:45:22Z | unknown x60 |
|
||||
| chest_ownership_{digits}.log | 60 | 179952 | 2026-03-27T20:59:30Z | 2026-08-12T01:18:15Z | unknown x60 |
|
||||
| configstore_log.txt | 4 | 699044 | 2026-08-13T00:18:01Z | 2026-08-13T00:18:01Z | unknown x4 |
|
||||
| connection_log.previous.txt | 4 | 1048280 | 2026-06-15T07:06:40Z | 2026-06-15T07:06:40Z | unknown x4 |
|
||||
| connection_log.txt | 4 | 66156 | 2026-08-12T01:18:13Z | 2026-08-12T01:18:13Z | unknown x4 |
|
||||
| connection_log_{n}.previous.txt | 8 | 2096872 | 2025-08-27T04:07:52Z | 2026-06-23T22:09:49Z | unknown x8 |
|
||||
| connection_log_{n}.txt | 16 | 1189424 | 2026-05-27T00:15:29Z | 2026-08-13T00:18:24Z | unknown x16 |
|
||||
| economy_{digits}.log | 60 | 11669828 | 2026-03-29T20:22:15Z | 2026-08-12T01:18:15Z | unknown x60 |
|
||||
| event_kill_{digits}.log | 60 | 6000 | 2026-03-27T19:37:41Z | 2026-08-12T01:18:15Z | unknown x60 |
|
||||
| famepoints_{digits}.log | 60 | 311680 | 2026-04-03T02:00:13Z | 2026-08-12T01:18:15Z | unknown x60 |
|
||||
| gameplay_{digits}.log | 60 | 4727592 | 2026-04-06T17:39:14Z | 2026-08-13T03:19:26Z | unknown x60 |
|
||||
| kill_{digits}.log | 60 | 321132 | 2026-04-05T02:33:31Z | 2026-08-12T01:18:15Z | unknown x60 |
|
||||
| login_{digits}.log | 60 | 528916 | 2026-04-05T02:33:44Z | 2026-08-12T23:43:03Z | unknown x60 |
|
||||
| loot_{digits}.log | 60 | 1519888 | 2026-03-27T19:37:51Z | 2026-08-12T01:18:23Z | unknown x60 |
|
||||
| network_objects_{digits}.log | 56 | 5600 | 2026-05-06T02:24:44Z | 2026-08-12T01:18:15Z | unknown x56 |
|
||||
| quests_{digits}.log | 60 | 50320 | 2026-03-27T19:37:41Z | 2026-08-12T01:18:15Z | unknown x60 |
|
||||
| raid_protection_{digits}.log | 60 | 13600 | 2026-03-27T19:37:52Z | 2026-08-12T01:18:24Z | unknown x60 |
|
||||
| sentry_{digits}.log | 60 | 6000 | 2026-03-27T19:37:41Z | 2026-08-12T01:18:15Z | unknown x60 |
|
||||
| server_notifications_{digits}.log | 60 | 6000 | 2026-03-27T19:37:41Z | 2026-08-12T01:18:15Z | unknown x60 |
|
||||
| service_log.txt | 4 | 145156 | 2026-08-12T01:18:13Z | 2026-08-12T01:18:13Z | unknown x4 |
|
||||
| stats_log.previous.txt | 4 | 1048448 | 2026-05-08T00:23:23Z | 2026-05-08T00:23:23Z | unknown x4 |
|
||||
| stats_log.txt | 4 | 615312 | 2026-08-12T23:43:24Z | 2026-08-12T23:43:24Z | unknown x4 |
|
||||
| steam_appid.txt | 4 | 24 | 2025-07-29T16:16:59Z | 2025-07-29T16:16:59Z | unknown x4 |
|
||||
| systemmanager.txt | 4 | 94240 | 2026-08-12T01:18:13Z | 2026-08-12T01:18:13Z | unknown x4 |
|
||||
| vehicle_destruction_{digits}.log | 60 | 3446040 | 2026-04-05T02:21:34Z | 2026-08-12T01:18:24Z | unknown x60 |
|
||||
| violations_{digits}.log | 60 | 303948 | 2026-03-27T19:37:41Z | 2026-08-12T01:18:15Z | unknown x60 |
|
||||
|
||||
## Representative sanitized line-shape observations
|
||||
|
||||
The first line-shape diagnostic successfully discovered the same process-adjacent log set and emitted redacted ASCII skeletons before Windows stdout encoding aborted the tail of that diagnostic. The authoritative file inventory above comes from the successful v4 diagnostic. The v2 prefix showed these structural properties without raw lines:
|
||||
|
||||
- `login_{digits}.log`, `admin_{digits}.log`, `gameplay_{digits}.log`, `loot_{digits}.log`, and related SCUM event logs are readable as text samples and include date-like prefixes.
|
||||
- `gameplay_{digits}.log`, `vehicle_destruction_{digits}.log`, and login fixtures carry coordinate-shaped `X=/Y=/Z=` tokens in some lines.
|
||||
- Several operational logs contain network-shaped tokens; parsers must strip or avoid network material before durable storage and logical fingerprints.
|
||||
- `connection_log*.txt`, `service_log.txt`, `stats_log.txt`, and `systemmanager.txt` are separate service/runtime log families and must not be conflated with SCUM gameplay event files.
|
||||
|
||||
## Read-safety notes
|
||||
|
||||
- Diagnostic only grouped file metadata and BOM fingerprints; no raw log line was emitted in the committed inventory.
|
||||
- Existing `scum-login-log-fixtures-2026-08-13.md` remains the source for redacted parser fixtures; this baseline records broader log inventory and structure.
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
# SCUM Current-Service SQLite Diagnostic Evidence — 2026-08-12
|
||||
|
||||
## Scope
|
||||
|
||||
- Evidence kind: operator-directed, server-local, read-only diagnostic discovery.
|
||||
- Execution target: `枣庄服务器` through the personal server-management MCP (`list_devices`, `test_connection`, `ssh_exec`).
|
||||
- Product boundary: Platform/plugin did not download or parse `SCUM.db`. The diagnostic script executed on the game server host and emitted only schema metadata, aggregates, hashes, and redacted samples.
|
||||
- Release boundary: this evidence supports adapter discovery. It does not replace the Platform durable Run job / typed Run envelope required before enabling database-backed production capabilities.
|
||||
|
||||
## Database Identity and Read Behavior
|
||||
|
||||
- One active `SCUM.db` candidate was found beside the running SCUM service metadata; no host path is recorded in this artifact.
|
||||
- Read mode: Python `sqlite3` URI `mode=ro`, `PRAGMA query_only=ON`; initial probes used a 2s timeout, and the cadence/lock probe used a 0.75s connection timeout plus `busy_timeout=250ms`.
|
||||
- Database size: initial schema probe `80,805,888` bytes; cadence probe `80,846,848` bytes.
|
||||
- SQLite metadata: `schema_version=765`, `user_version=57`, `journal_mode=wal`, `page_size=4096`, `page_count=19728`, `freelist_count=1`.
|
||||
- Object inventory: `293` schema objects, `161` tables.
|
||||
- Safe-read timing observed: full schema inventory `61ms`; focused groups `3ms`, `13ms`, `3ms`; join/range metrics `25ms`; follow-up join/meaning probes `27.3ms` and `5.68ms`; cadence probe p95 timings were `15.576ms` for all-entity coordinate range, `0.603ms` for player coordinate range, `0.438ms` for bounded player-position fingerprint, `0.316ms` for vehicle coordinate range, and `0.069ms` for spawn-location range. No read lock/busy failure was observed during these diagnostics.
|
||||
- Fingerprints: full schema objects `57e34ee72660d7e4334644ee70cd6d285ac7961a3964fafdc3956d74e88dfa4f`; focused table groups `12a34e49f851879ae71ba287719c8d95019909f3060e823be4ce0973ce764841`, `a856bc4e105ab0a5e34b758237d3f96cff0ba5a65b38e7cf1eb81ab16b40caf4`, `60c97a8c782086c4b2600eb3a9b29c3074b971d5fdc6cf54137ce48e0970b26e`; join/range metrics `4905d09b70303b42cfb8e7fc936fe0df2065d7100c43e92ef79a464958249af4`.
|
||||
|
||||
## Coordinate Cadence, Snapshot, and Safe Limits
|
||||
|
||||
- Cadence probe: `10` read-only samples over `27.175s` against the same single active database candidate; the database file modification time changed during `5` samples, but `schema_version`, `data_version`, all-entity coordinate range, player coordinate range, player position fingerprint, vehicle coordinate range, and spawn range remained stable. Aggregate digest: `59fe6dfd8c2dc41d3533a9a4d860e82c6a5486edf079049f5c66436d67d7b100`.
|
||||
- Snapshot consistency: a repeated player coordinate aggregate inside one read transaction returned identical results (`repeat_equal=true`, digest `38b1865f062aa27181f4fb5c2301b2f52a5e312bd9b4f085c62b1df6d27284eb`).
|
||||
- Current coordinate ranges in the cadence probe: all `entity` rows `63370`, x `-901009.4375..612580.0625`, y `-883992.5625..615977.375`, z `-3573.64990234375..102111.6640625`, null coordinate count `0`; joined prisoners `72`, x `-872217.6875..567603.0625`, y `-843655.8125..554482.125`, z `221.87356567382812..82958.546875`, `prisoner.last_save_time` `1773202437..1786549259`; vehicle entities `313`, x `-898438..601692.3125`, y `-881533.25..605226.5`, z `-84.04053497314453..98312.640625`, functional vehicles `303`, distinct asset IDs `15`.
|
||||
- Base/flag coordinate support: `base` has `5` rows with x `-381908.21875..349512.28125` and y `-565472..538980.75`; `base_element` has `1533` rows with x `-382911.5..349512.28125`, y `-566506.125..538980.75`, and z `570.5759887695312..37294.0078125`; `base_element_flag` has `5` rows and no direct coordinate columns, so a flag/territory adapter must use the verified base/base-element relationship rather than inventing separate flag coordinates.
|
||||
- Spawn-location range remains non-current-position evidence: `78` rows, x `-872218..567603`, y `-843656..554482`, z `223..115567`, `2` distinct observed types.
|
||||
- Safe polling limits derived for adapter design: SQLite reads should use `mode=ro`, `PRAGMA query_only=ON`, `busy_timeout<=250ms`, operation timeout `<=2000ms`, single-query target `<=750ms`, result bytes `<=262144`, and initial row limits of `500` player positions, `1000` vehicle positions, and `200` flags/base rows unless a versioned adapter proves tighter or broader bounds.
|
||||
- Realtime-map conclusion: SCUM.db writes occurred during the sample window, but the verified position aggregates/fingerprints did not change. The database source does not currently prove a sub-10s realtime-map cadence; any sub-10s claim requires a separately declared and verified companion position source. Until then, the product must present the measured database cadence honestly and avoid fabricated intermediate motion or unsafe polling.
|
||||
- Non-blocking probe warnings: optional exploratory fingerprint/ownership subqueries that assumed unverified vehicle row and flag-owner column names were rejected safely. Those warnings did not affect the successful coordinate/cadence aggregates and do not enable vehicle ownership, flag ownership, or write capabilities.
|
||||
|
||||
## Evidence Matrix
|
||||
|
||||
| Area | Current-service evidence | Remaining ambiguity |
|
||||
| --- | --- | --- |
|
||||
| External player identity | `user` has `74` rows with `id TEXT` primary key, `id_type`, `provider`, `last_login_time`, `creation_time`, `is_banned`, and network-address field present but not persisted in this artifact. Follow-up aggregates show all `74` users have `id_type=Steam`, provider `Server`, and non-null identity/login/banned fields. | Product APIs must hash or fence external IDs where appropriate and must never expose IP/network material. |
|
||||
| Player profile join | `user_profile` has `73` rows; all `73` join to `user` through `user_profile.user_id -> user.id`; all profiles have `type=1`. `user_profile.prisoner_id -> prisoner.id` and `prisoner.user_profile_id -> user_profile.id` both resolve `72` profiles. Indexes on `(user_id,type,name)`, `type`, and `prisoner_id` were observed. | Profile `type=1` is observed but not independently named; adapter labels must stay version-scoped rather than using reference-project meanings. |
|
||||
| Character/prisoner join | `prisoner` has `72` rows and `prisoner_entity` has `72` rows mapping `prisoner_id -> prisoner.id` and `entity_id -> entity.id`. Join metrics: `73` profiles, `73` with user, `72` with prisoner, `72` with prisoner entity, `72` with entity; exactly one profile has null `prisoner_id`. | One profile has no current prisoner/entity. Online state must still come from authenticated login/session evidence, not database timestamps alone. |
|
||||
| Character XML / payload | `user_profile.template_xml` is present and non-null for `73/73` profiles, length range `2541–2673`; samples were hash+length only. `prisoner_skill.xml` has `72/1656` non-null rows, length `15–65`; `item_entity.xml` has `42304/62844` non-null rows, length `136–4781`. | `user_profile.template_xml` is the verified profile-level XML source candidate, but write semantics, named attributes, and `855` mapping remain unverified. |
|
||||
| Player coordinates | `entity` has `63364+` rows with `location_x/y/z`, `rotation_x/y/z`, scale, flags, class, and optional BLOB data. The cadence probe observed `72` joined prisoners with x `-872217.6875..567603.0625`, y `-843655.8125..554482.125`, z `221.87356567382812..82958.546875`; `prisoner.last_save_time` range `1773202437..1786549259`. | Database writes occurred during the cadence window, but verified player coordinates did not change; do not claim sub-10s realtime from SCUM.db without a companion source. |
|
||||
| Squad | `squad` has `7` rows; `squad_member` has `18` rows with `squad_id`, `user_profile_id`, `rank`; all members join to both squad and profile. Rank distribution: `1:6`, `2:2`, `3:4`, `4:6`; squad sizes are `1` member for `3` squads, `2` members for `2` squads, `3` members for `1` squad, and `8` members for `1` squad. | Rank meanings / leader semantics are not present in the probed schema. Keep rank labels neutral and leader unknown unless a versioned adapter proves the mapping. |
|
||||
| Vehicles | `vehicle_spawner.vehicle_entity_id -> vehicle_entity.entity_id -> entity.id` resolves all `313` vehicle spawners; `15` distinct `vehicle_asset_id` values and one redacted alias value were observed. `is_vehicle_functional` distribution is `303` true / `10` false and all spawners are marked automatically created. Vehicle entity coordinate range: x `-898438..601692.3125`, y `-881533.25..605226.5`, z `-84.04053497314453..98312.640625`. | Vehicle ownership meanings are absent from the verified join. Status can be limited to the probed functional/automatic-created fields; owner stays null. |
|
||||
| Flags / bases | `base_element_flag.element_id -> base_element.element_id -> base.id` resolves all `5` flags. All `5` flags have an owner profile through the verified flag/base join from the join-meaning probe; the coordinate-cadence probe separately confirmed `base` and `base_element` coordinate ranges while `base_element_flag` itself has no direct coordinate columns. | Profile ownership and coordinate support are verified only through the probed joins. Squad territory ownership remains separately gated because one owner has no squad membership and membership may not be the same fact as base ownership. |
|
||||
| Economy / balances | `bank_account_registry.account_owner_user_profile_id -> user_profile.id` resolves all `73` accounts; `bank_account_registry_currencies.bank_account_id -> bank_account_registry.id -> account_owner_user_profile_id` resolves all `146` currency rows. The nullable `user_profile_id` columns in both bank tables are entirely null. Currency distribution: type `1` has `73` rows, `account_balance` range `-3000..830328`; type `2` has `73` rows, range `0..14636`. | Currency type meanings, units, safe command/readback semantics, and gift item aliases remain unverified. Query adapters may expose numeric type only behind version-scoped labels until commands/readback are proven. |
|
||||
| Spawn/location table | `prisoner_spawn_location` has `78` rows with `location_x/y/z`, rotation, velocity, `type`, optional `shelter_id`; coordinate range roughly matches player entity bounds. | This is spawn-location evidence, not current position evidence. |
|
||||
|
||||
## Redaction Notes
|
||||
|
||||
- Player names, squad names/messages, map names, aliases, XML, BLOB payloads, tokens, and network-address material were represented only as hash+type+length when sampled.
|
||||
- Three player join samples were retained only as hash of external player id plus numeric profile/prisoner/entity IDs, timestamps, fame points, and coordinates.
|
||||
- Follow-up probes returned only aggregate counts, distributions, nullable counts, column names, coordinate ranges, and timing; no raw rows, host paths, XML, player/squad names, IP/network material, commands, credentials, or sockets were recorded.
|
||||
- No raw SQL, host database path, credentials, direct sockets, raw XML, raw player names, or raw IP/network identifiers are recorded here.
|
||||
|
||||
## Task 2.8 Confirmation Pass — 2026-08-13
|
||||
|
||||
- Economy command confirmation: no Fame, normal-currency, or gold command is confirmed safe for the new adapter contract. The current SCUM live-data manifest contains only `schemaVersion`, `probe`, and `capabilityGates`; it declares no `typedRconTemplates` asset, no digest-referenced economy command, and no confirmation schema. The `economy-command.write` gate remains `disabled` / `missing` with the safe reason that Fame/currency command and readback confirmation are still awaiting verification. The current-service database evidence proves only bank-account numeric rows and currency type distributions; it does not prove currency labels, units, command execution, or readback semantics. The read-only reference project contains command-shaped hypotheses (`#SetCurrencyBalance Normal`, `#SetCurrencyBalance Gold`, `#setFamePoints`), but those are queued command strings without current-service confirmation evidence and SHALL NOT be shipped as verified typed assets.
|
||||
- Gift aliases and transports: no gift item alias or executable gift transport is confirmed real for this change. The live-data manifest declares no `giftCatalogs`, no gift transport template, and no digest-referenced catalog/transport pair. The existing bridge surface still lists legacy `reward.deliver` / `player.notify` handlers, and the platform domain contains a small hard-coded catalog (`bandage`, `water-bottle`, `improvised-spear`), but those declarations are not versioned live-data assets, have no current-service alias evidence, no generated Run package digest, and no conclusive per-item receipt contract. The read-only reference project stores arbitrary gift code JSON and queues free-form `#<code><!{[]}>...` commands before conclusive game-effect confirmation; it is therefore a hypothesis source only, not verified alias or transport evidence.
|
||||
- Map asset and transform authorization: no distributable SCUM map asset or coordinate transform is authorized yet. The live-data manifest declares no `mapAssets`, no map image digest, no transform asset path/digest, and no metadata schema reference. The cadence probe verifies world coordinate ranges, and the read-only reference project contains a 256-grid area transform using observed bounds, but neither proves a first-party redistributable map image, tested transform fixtures, or adapter-compatible asset identity. The realtime map SHALL remain unavailable for verified rendering until a digest-referenced map asset and transform are packaged and tested.
|
||||
- `855` preset mapping: no operator-confirmed `855` mapping exists. The current-service evidence confirms `user_profile.template_xml` as the profile-level XML source candidate, and the read-only reference parser names XML attributes such as `Strength`, `Constitution`, `Dexterity`, `Intelligence`, cosmetic attributes, and skill nodes. However, neither the current-service probe, repository manifest, nor reference search produced a reviewed `855` label mapping to named attributes/values. Reference behavior also indicates attribute changes may be activated by killing the prisoner, which remains explicitly forbidden as an implicit chained action. The `profile-xml.write` gate remains disabled; `855` SHALL stay absent until an operator-confirmed preset expands into explicit before/after named attributes with separate activation evidence if any.
|
||||
|
||||
## Resulting Gates
|
||||
|
||||
- Task 2.5 schema capture is satisfied for discovery: `sqlite_schema`, read-only PRAGMA metadata, indexes, foreign keys, declared types, cardinalities, and redacted samples were captured for the candidate sources.
|
||||
- Task 2.6 join/meaning verification is satisfied for discovery: external identity, profile/prisoner/entity joins, flag/base joins, vehicle identity joins, bank-account joins, nullable fields, and the profile XML source candidate are recorded. Unproven rank leader semantics, squad-territory ownership, currency labels/units, and write meanings remain explicitly gated rather than guessed.
|
||||
- Task 2.7 cadence/safe-read discovery is satisfied: repeated read-only sampling measured coordinate ranges, latency, lock/busy behavior, snapshot consistency, safe limits, and the need for a companion position source before any sub-10s realtime-map claim.
|
||||
- Task 2.8 confirmation is satisfied as a negative gate: economy commands, gift aliases/transports, map asset/transform authorization, and the `855` preset mapping were checked separately and none is verified enough to enable. The corresponding write/map capabilities remain disabled until explicit operator/current-service and digest-referenced adapter evidence exists.
|
||||
- Database-backed SCUM read/write capabilities remain disabled until the corresponding versioned adapters and Run durable execution envelopes are implemented and accepted.
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
# SCUM Durable Run Schema Probe Evidence — 2026-08-12
|
||||
|
||||
## Scope
|
||||
|
||||
- Evidence kind: product-path durable Run job result accepted by Platform.
|
||||
- Target server: `枣庄服务器` via the active authenticated Run binding `server-run-server-scum-1785923898033`.
|
||||
- Boundary: Platform/plugin/browser did not download or parse `SCUM.db`; the database probe was produced by Run as a typed, redacted `sqlite.schema-probe` envelope and then persisted by Platform.
|
||||
- Redaction: this artifact intentionally omits host paths, database paths, credentials, sockets, raw SQL, raw rows, raw XML, raw player names, and network material.
|
||||
|
||||
## External Run Deployment Evidence
|
||||
|
||||
- Independent Run repository: `git@git.npc0.com:admin343/run.git`.
|
||||
- Tested Run fix: commit `8fe6f9b` (`Fix SQLite probe data target mapping`).
|
||||
- Run verification performed in the independent Run repository: focused data-target/runtime tests and `go test ./...` passed before deployment.
|
||||
- Installed distribution on `枣庄服务器`: `run-dist-server-scum-1785923898033-windows-amd64-1-zao-zhuang-data-target-run-fix-2026081-6988495348508259730`.
|
||||
- Distribution checksum: `sha256:a5ac9fe31ed0e0f595e70e3d3322f44aa81bd165183530e4be6939aff81c3016`.
|
||||
- Runtime capability evidence: the active endpoint advertises `remote.run.db.sqlite.probe` and `remote.run.db.sqlite.query` after installation.
|
||||
|
||||
## Platform Acceptance Evidence
|
||||
|
||||
- Durable job: `job-remote-adapter-server-scum-1785923898033-7249327407638289501`.
|
||||
- Platform terminal state: `succeeded` with progress `100` and message `SQLite schema probe completed`.
|
||||
- Terminal time: `2026-08-12T12:29:24.857542Z`.
|
||||
- Probe observed time: `2026-08-12T12:17:39.0088015Z`.
|
||||
- Probe status: `succeeded`.
|
||||
- Source fingerprint: `sha256:d8f3e2f5e9c8241f55b931008309a7ab5f241118a82cbd3620ddedf233e74c13`.
|
||||
- Schema fingerprint: `sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0`.
|
||||
- Result digest: `sha256:ef13678df4add731c758bba157627dc8af80138a69476facd81bbe354c31d7f1`.
|
||||
- Schema object count: `161`.
|
||||
- Safe error: empty / not retryable.
|
||||
|
||||
## Platform Decode Fix
|
||||
|
||||
- Run emits `sourceFingerprint` in successful SQLite schema-probe results.
|
||||
- Platform DTO/domain/validator handling now accepts and persists `sourceFingerprint` only when it is a safe digest/fingerprint.
|
||||
- Validator coverage rejects raw path-shaped values such as a host database path and permits bounded generic Run `data_target_*` safe error codes without allowing path material.
|
||||
|
||||
## Server-Management SSH Verification
|
||||
|
||||
- The personal server-management MCP inventory found `枣庄服务器` with device id `FyBDIohqPhRx7Cia`.
|
||||
- `test_connection` succeeded for `枣庄服务器`.
|
||||
- `ssh_exec` was used for bounded, read-only PowerShell status checks only; no SCUM database query or file copy was performed.
|
||||
- Remote process summary at `2026-08-12T14:13:56.4937358Z`: `SCUMServer.exe` process count `1`.
|
||||
- Run journal summary: scanned `1` job journal, target job active count `0`, target pending result count `0`, total active count `0`, total pending count `1`.
|
||||
- Interpretation: the durable schema-probe job is not stuck in the remote Run active queue or pending-results spool after Platform accepted the typed result. The remaining unrelated pending result, if any, is outside this evidence item.
|
||||
|
||||
## Resulting Gates
|
||||
|
||||
- This proves the product acceptance path for schema probing: Platform durable job → authenticated active Run binding → Run-side typed/redacted SQLite schema-probe envelope → Platform validation and persistence.
|
||||
- This does not prove player/squad/vehicle/flag/position query adapter compatibility, rank meanings, currency semantics, map transform, gift aliases/transports, or `855` named-attribute mapping.
|
||||
- Database-backed player/squad/map/gift/write capabilities remain disabled until the versioned adapters and query/mutation contracts in later task groups are implemented and matched against this evidence matrix.
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
# SCUM Login Log Fixture Evidence - 2026-08-13
|
||||
|
||||
## Scope
|
||||
|
||||
- Change: `replace-scum-projections-with-real-data-management`
|
||||
- Task: 3.3, sanitized authentic login-log fixtures from the active service
|
||||
- Device: `枣庄服务器` (`FyBDIohqPhRx7Cia`)
|
||||
- Server instance: `server-scum-1785923898033`
|
||||
- Run binding: `server-run-server-scum-1785923898033`
|
||||
- Plugin: `game.scum` `0.1.6`
|
||||
- Probe path: personal server-management MCP `list_devices`, `test_connection`, then bounded `ssh_exec`
|
||||
- Observed time: `2026-08-12T23:44:07.9106530Z`
|
||||
|
||||
The diagnostic ran only on the configured active server through the server-management MCP. It returned no host paths, raw IP addresses, raw player names, raw Steam IDs, raw coordinates, credentials, sockets, SQL, RCON, or database content. Source and file identities are SHA-256 fingerprints of server-local paths and file metadata.
|
||||
|
||||
## Source Discovery
|
||||
|
||||
- `test_connection` succeeded for `枣庄服务器`.
|
||||
- `SCUMServer.exe` was observed running during the diagnostic window.
|
||||
- Recursive log discovery found SCUM `login_{date}{digits}.log` files under the active service log root.
|
||||
- The newest login log is currently empty, so the next Run log-source tail must handle a zero-byte active file and later append/rotation cleanly.
|
||||
|
||||
```json
|
||||
{
|
||||
"latestLoginLog": {
|
||||
"sourceIdentity": "sha256:752c3ee3789fe73b80245dfcb97776db27f977c4350c3b50516278afbecb9dad",
|
||||
"streamGeneration": "sha256:57b5be4818757e4d64070e5e0f026dbd471bdba189d0426a38b618423f7e1439",
|
||||
"nameTemplate": "login_{date}{digits}.log",
|
||||
"bytes": 0,
|
||||
"lastWriteUtc": "2026-08-12T01:18:15.6090680Z"
|
||||
},
|
||||
"loginFilesSeen": 30,
|
||||
"filesRead": 10,
|
||||
"linesRead": 30,
|
||||
"encoding": "utf-16le"
|
||||
}
|
||||
```
|
||||
|
||||
## Parser Binding
|
||||
|
||||
The production parser asset is not implemented yet. These fixtures bind the pending parser profile that task 3.4 must implement and digest-reference before the parser can create durable players.
|
||||
|
||||
```json
|
||||
{
|
||||
"parserKey": "scum-login-log-parser.pending-real-fixture-v1",
|
||||
"parserVersion": "pending-scum-login-log-v1",
|
||||
"parserDigest": "sha256:5bb528cb9f6e04d8d8b819db3a71f855569c78a5135f22a982301301ae1da50a",
|
||||
"lineEncoding": "utf-16le",
|
||||
"acceptedTemplate": "{timestamp}: '{network_redacted} {external_player_id}:{display_name}({profile_local_id})' logged {in|out} at: X={coordinate} Y={coordinate} Z={coordinate}",
|
||||
"privacyRules": {
|
||||
"networkMaterial": "strip before durable storage and logical fingerprinting",
|
||||
"externalPlayerId": "hash in evidence; store only through the later typed ingestion contract",
|
||||
"displayName": "hash in evidence; parser may emit display name only through the safe event schema",
|
||||
"coordinates": "redact in parser fixture evidence; map capability remains separately gated"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The logical event identity is derived from server ID, event type, and the normalized non-network event line. It intentionally excludes `sourceIdentity`, `streamGeneration`, and `sequence`, so replaying the same logical line under a later copy-truncate or rotation generation remains idempotent.
|
||||
|
||||
## Fixtures
|
||||
|
||||
All fixture rows are from active-service `login_{date}{digits}.log` files. Values below are already sanitized; the original line content was not written to the repository.
|
||||
|
||||
| # | Event | Source Identity | Generation | Seq | Line Digest | External Player Hash | Logical Event Identity | Occurrence Local Time |
|
||||
|---|---|---|---|---:|---|---|---|---|
|
||||
| 1 | `scum.login` | `sha256:485b0e95ad608bb5f1dd2a6dfe74856526ec3a0c2f7eaa80b64ce31aadd06331` | `sha256:99e8e26fb62ea9b143024dc78e87b6727c87c32d79c11a4e482ec7157b57fd30` | 3 | `sha256:4450a172273ba476f9e472ff3afcdf6a8b6e069c37e274ed95fa23a8a94b0d2e` | `sha256:2a740d7265b07f2b886e7a0f1f249321d040557f42b19c83759ffbb620bfb6e7` | `sha256:8a93cfb9deb8f82528bae3bc2273d0e0f89ffe2d4761277f994cedda84533225` | `2026.07.05-05.39.10` |
|
||||
| 2 | `scum.logout` | `sha256:485b0e95ad608bb5f1dd2a6dfe74856526ec3a0c2f7eaa80b64ce31aadd06331` | `sha256:99e8e26fb62ea9b143024dc78e87b6727c87c32d79c11a4e482ec7157b57fd30` | 4 | `sha256:2e7d6690260854d3f38c7c2fd97bc8b297d718a55ca192e3e14ed79268425dde` | `sha256:2a740d7265b07f2b886e7a0f1f249321d040557f42b19c83759ffbb620bfb6e7` | `sha256:ee80c7b4b5e1176c11d4c0a3b3393182ab971bc8bb50f92ac9384b26255a6e2d` | `2026.07.05-07.21.15` |
|
||||
| 3 | `scum.login` | `sha256:5b1630d3747f317fb0d2000f3c7072cd202e358e59ec43f4efff9d192164210c` | `sha256:6aed8ccee83260b16e771ed8a8d49fd26d581a9307b5c01b8294ddcc5906624f` | 3 | `sha256:450b0e4cd4e24c3f9e21b6812103214936b3b58c61e8a887d3458c9a03b2ea6d` | `sha256:f2d4c03c66c9ef6cf9339fa983ddb86ec64a2d41f4756e0831a522c604f87845` | `sha256:b35fc0e99ed878f47514bf368c472bc137a109b4ca1f7dfc84529717e1cb1f02` | `2026.06.19-12.30.45` |
|
||||
| 4 | `scum.logout` | `sha256:5b1630d3747f317fb0d2000f3c7072cd202e358e59ec43f4efff9d192164210c` | `sha256:6aed8ccee83260b16e771ed8a8d49fd26d581a9307b5c01b8294ddcc5906624f` | 4 | `sha256:442994383d47561339be31149d0c6836430f528aedf0cf72ba51788658f24832` | `sha256:f2d4c03c66c9ef6cf9339fa983ddb86ec64a2d41f4756e0831a522c604f87845` | `sha256:362e7f05ac8015ea7fdf58c5c5f9709154b331c1d15ca56fff0121058e4d3f59` | `2026.06.19-16.36.22` |
|
||||
| 5 | `scum.login` | `sha256:5b1630d3747f317fb0d2000f3c7072cd202e358e59ec43f4efff9d192164210c` | `sha256:6aed8ccee83260b16e771ed8a8d49fd26d581a9307b5c01b8294ddcc5906624f` | 5 | `sha256:853ded422a039c697cb88760b3bf5292810dbac887e4464d84959f92afcc3a82` | `sha256:a6a353c4223497d1a43631104b31c20884a4cf0ce8cbb972398e885682c7b538` | `sha256:dd93eb0f7e4455708b2defdab69a39814bfb84efb84c99a17306715f6597258a` | `2026.06.20-06.36.46` |
|
||||
| 6 | `scum.logout` | `sha256:5b1630d3747f317fb0d2000f3c7072cd202e358e59ec43f4efff9d192164210c` | `sha256:6aed8ccee83260b16e771ed8a8d49fd26d581a9307b5c01b8294ddcc5906624f` | 6 | `sha256:18a014c7e3dccdd1a3b5179fa49bd1fb88d47bab1f1c9ce1ff3114c908999d61` | `sha256:a6a353c4223497d1a43631104b31c20884a4cf0ce8cbb972398e885682c7b538` | `sha256:a1a8a46507b6cabf03a94220ddbe1437c181b7f6ba306117594acf095ba3d7cf` | `2026.06.20-06.44.55` |
|
||||
| 7 | `scum.login` | `sha256:5b1630d3747f317fb0d2000f3c7072cd202e358e59ec43f4efff9d192164210c` | `sha256:6aed8ccee83260b16e771ed8a8d49fd26d581a9307b5c01b8294ddcc5906624f` | 7 | `sha256:1d03f4ea4ef8acbb3aeab390215e15be03fb7c4563956827ab933dbf4ba66139` | `sha256:2f260f8e697d93c26fda3ec9858c8b9e03a7a95f36378c58d579030f9c2914df` | `sha256:6cd52f703336f4ba63011a1711e6fcd0cd5839a577f178337f9170475bf25219` | `2026.06.20-12.30.26` |
|
||||
| 8 | `scum.logout` | `sha256:5b1630d3747f317fb0d2000f3c7072cd202e358e59ec43f4efff9d192164210c` | `sha256:6aed8ccee83260b16e771ed8a8d49fd26d581a9307b5c01b8294ddcc5906624f` | 8 | `sha256:9ba905e6a1b352aa8489ee3be2476e525f3888fbad05240e772d163bb22a517a` | `sha256:2f260f8e697d93c26fda3ec9858c8b9e03a7a95f36378c58d579030f9c2914df` | `sha256:943962513b85365cd5cd276e70b01c6882f203037e3a3eb6195d56cfbb0a936d` | `2026.06.20-12.32.40` |
|
||||
|
||||
## Fixture Conclusions
|
||||
|
||||
- Authentic login files are UTF-16LE and use the `logged in/out at` template shown above.
|
||||
- A single fixture row contains raw network material, external player ID, display name, local profile ID, and coordinates; fixture evidence stores only hashes/placeholders for those values.
|
||||
- Transport cursor identity is `(sourceIdentity, streamGeneration, sequence)`.
|
||||
- Logical event identity is separate and stable across rotation overlap because it excludes source identity, stream generation, sequence, and network material.
|
||||
- The newest active login file is empty; Run tailing must begin from a complete-line boundary and handle later appends without creating events from the header-only or zero-byte files.
|
||||
- Existing `companion/events.go` still parses only the invented stdout/stderr `SCUM LOGIN/LOGOUT` format; task 3.4 must replace/add a versioned parser for these authentic UTF-16LE file-tail fixtures before enabling login-driven player creation.
|
||||
@@ -1,45 +0,0 @@
|
||||
## Why
|
||||
|
||||
The current SCUM plugin presents projection and Workflow concepts as if they were real operations, but its declared SCUM.db queries contain no SQL, its read workflows do not dispatch Run jobs, its login parser does not understand the real SCUM login-log format, and several player, map, gift, and `855` controls fabricate data or evidence. The product needs a smaller SCUM management surface backed only by current-server login logs, verified SCUM.db facts, durable local records, and permission-checked writes.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **BREAKING**: Supersede the projection/observation/Workflow architecture introduced by `integrate-real-scum-ops-workflows`; remove SCUM Workflow/status product APIs, repositories, services, page actions, refresh/audit controls, freshness terminology, and the `Workflow 状态` page.
|
||||
- **BREAKING**: Remove the built-in `管理` tab from the SCUM server detail navigation, make the first SCUM plugin page the default, and relocate necessary deployment, metadata, and administrator controls to existing server-list actions and a server-settings drawer.
|
||||
- Discover the schema of the current bound service through a read-only Run probe before defining production queries; do not treat an unbound copied/reference `SCUM.db`, `scum_robot/model/scum_server.go`, historical cache, or guessed joins/enums as authoritative. A Run-created execution snapshot is acceptable only when it is fenced to the active binding/database identity, timestamped, checksummed, and invalidated on rebinding or schema change.
|
||||
- Make the SCUM plugin own versioned login-log parsers, SQLite query assets, result schemas, coordinate metadata, RCON command templates, and controlled mutation declarations. Run remains a generic bounded executor and never hardcodes SCUM keys, tables, commands, or paths.
|
||||
- Store players, sessions, squads, squad members, vehicles, flags, current positions, gifts, deliveries, and sync cursors in dedicated platform-owned local records rather than a monolithic metadata snapshot or fabricated product projection.
|
||||
- Automatically create/update players and login sessions from authentic login/logout log events, and automatically synchronize verified SCUM.db facts in the background without user-facing refresh, projection, or audit buttons.
|
||||
- Rebuild user management as a searchable, filterable, paginated player roster with detail and explicit edits; remove player-intelligence, shared-IP, automatic-risk, alias-history, fake values, and hard-coded increment actions from the SCUM product flow.
|
||||
- Rebuild squad management and the realtime map from verified squad/member/vehicle/flag/coordinate data, a declared SCUM map asset, and a tested coordinate transform; never infer ownership or coordinates that the source cannot prove.
|
||||
- Rebuild gift management as server-scoped local CRUD plus bounded, idempotent delivery records whose success is recorded only after a conclusive Run result.
|
||||
- Preserve the AI assistant for plugin configuration and player-operation drafts. Manual and AI-originated writes use the same effective-user permissions, reviewable before/after diff, allowlisted plugin action, Run execution path, and read-after-write confirmation.
|
||||
- Replace the fake `player.attribute.855.set` integer column with named, version-scoped character attributes backed only by a targeted patch of the profile XML source confirmed by the current-service adapter; the patch preserves unknown XML content, and `855` may remain only as a user-confirmed preset label.
|
||||
- Remove all product copy and controls for projection, observation, stale projection, refresh real data, refresh projection, initiate audit, pending-review counters, and Workflow status while retaining non-product authorization, idempotency, safety, and internal audit evidence for writes.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `scum-live-data-sync`: Authentic login-log ingestion, current-service schema discovery, plugin-owned SQL assets, generic Run execution, automatic synchronization, and dedicated local SCUM records.
|
||||
- `scum-player-management`: Lean player roster/session management, search/filter/pagination/detail contracts, null-for-unknown semantics, and explicit player editing surfaces.
|
||||
- `scum-squad-management`: Verified squad, member, leader/rank, flag, territory, search, pagination, and detail behavior without inferred relationships.
|
||||
- `scum-realtime-map`: Verified player, vehicle, flag/territory coordinates, a distributable map asset, tested transforms, and automatically updated layers without fabricated positions.
|
||||
- `scum-gift-management`: Server-scoped gift package CRUD, eligibility, bounded delivery, idempotency, result handling, and delivery history.
|
||||
- `scum-controlled-writes`: Plugin-owned RCON and database/XML mutation declarations, effective-user permissions, manual/AI parity, safety guards, and confirmation requirements.
|
||||
- `scum-real-data-management-surface`: The exact SCUM navigation, AI-assistant preservation, server-settings relocation, and removal of projection/Workflow/audit-refresh product concepts.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- None. The main baseline contains no SCUM capability that can validly receive a `MODIFIED` or `REMOVED` delta. The completed-but-unarchived SCUM change artifacts remain historical implementation records: this change uses unique `ADDED` capability names and explicitly supersedes those old projection/Workflow/product requirements. They must not later be synchronized into the main baseline as current requirements; any history consolidation is a separate reviewed archival task using skip-specs or an equivalent non-resurrection path.
|
||||
|
||||
Supersession mapping: `scum-real-data-projections` is replaced by live sync plus the player/squad/map stores; `scum-workflow-automation` is retired; `scum-controlled-operations` and `scum-player-state-patch` are replaced by `scum-controlled-writes`; the old `scum-product-surface` is replaced by `scum-real-data-management-surface`; `scum-game-player-intelligence` is reduced to the lean player capability; `scum-map-trajectories` is replaced by current-position map behavior with trajectories removed; and `scum-versioned-gift-catalog` is replaced by server-local gift management and conclusive delivery receipts.
|
||||
|
||||
## Impact
|
||||
|
||||
- `platform/`: new SCUM data models and normalized repositories/migrations, automatic sync services, Run job completion handling, typed resource and mutation APIs, authorization checks, removal of projection/Workflow domains and endpoints, and migration away from SCUM snapshot fields.
|
||||
- `platform_web/`: SCUM navigation/default section, server-settings relocation, plugin host contracts, API types/clients, player/squad/map/gift pages, permission-aware edit flows, AI drafts, and removal of banned copy/actions.
|
||||
- `plugins/examples/scum-server-plugin/` and plugin SDK/schema validation: versioned log parser/query/mutation/command assets, result schemas, permissions, map metadata, page bundles, and removal of Workflow declarations.
|
||||
- External `git@git.npc0.com:admin343/run.git`: generic read-only SQLite template execution, safe result envelopes, plugin-owned mutation execution, and current-service schema-probe support. No `run/` source tree is added to this repository.
|
||||
- Persistence and deployment: dedicated local tables require explicit migration/rollback handling; old SCUM projection/Workflow snapshot data is not authoritative and is not migrated as real game facts.
|
||||
- Security: browser APIs continue to exclude SQL, raw XML, database paths, credentials, sockets, raw RCON, and host paths; AI gains no independent execution authority.
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Effective-user permission enforcement
|
||||
Every SCUM state-changing request SHALL be authorized against the current user's effective target-server permission at UI presentation, request creation, review/confirmation, and dispatch, with the backend as the authoritative enforcement point.
|
||||
|
||||
#### Scenario: Read-only operator opens a writable surface
|
||||
- **WHEN** a user has `server.game-client.read` but lacks the required write permission
|
||||
- **THEN** data remains readable, write controls are hidden or disabled with a textual reason, and a direct API attempt is forbidden
|
||||
|
||||
#### Scenario: Economy command is requested
|
||||
- **WHEN** a user edits Fame, cash, or gold
|
||||
- **THEN** Platform requires `server.game-client.command` and target-server access before accepting the named-field request
|
||||
|
||||
#### Scenario: Gift delivery is requested
|
||||
- **WHEN** a user sends a declared gift package to a server-local player
|
||||
- **THEN** Platform requires `server.game-client.command`, target-server access, and the gift capability's eligibility/idempotency checks before dispatch
|
||||
|
||||
#### Scenario: Database/XML attribute edit is requested
|
||||
- **WHEN** a user edits a maintenance-level character attribute or preset
|
||||
- **THEN** Platform requires `server.game-client.maintenance`, target-server access, a synchronous explicit danger confirmation, and the declared safety gates before dispatch; it SHALL NOT create a separate platform-admin approval domain or approval queue
|
||||
|
||||
#### Scenario: Plugin page receives permissions
|
||||
- **WHEN** Platform Web constructs the plugin page host context
|
||||
- **THEN** it supplies the current session's effective permissions rather than treating manifest-declared permissions or callback presence as proof of authority
|
||||
|
||||
### Requirement: Manual and AI write-path parity
|
||||
Manual forms and AI/Agent-originated SCUM changes SHALL create the same named-field draft and SHALL use the same allowlist, validation, effective-user authorization, review, dispatch, and confirmation service.
|
||||
|
||||
#### Scenario: AI suggests an allowed player change
|
||||
- **WHEN** a user with the required write permission asks AI to change a declared player field
|
||||
- **THEN** AI produces a reviewable target/player/field/current/proposed diff and no execution occurs until the user explicitly confirms it
|
||||
|
||||
#### Scenario: AI requester lacks write permission
|
||||
- **WHEN** AI can generate or display a suggestion for a user who lacks the field's write permission
|
||||
- **THEN** the apply action remains unavailable and an API attempt is forbidden; AI receives no component-principal bypass
|
||||
|
||||
#### Scenario: AI invents a field or protected payload
|
||||
- **WHEN** an AI response contains an undeclared field, raw SQL, raw XML, raw RCON, path, credential, or arbitrary command
|
||||
- **THEN** Platform rejects the draft and stores no executable protected payload from it
|
||||
|
||||
### Requirement: Plugin-owned immutable write assets
|
||||
The SCUM plugin SHALL own versioned typed command templates and guarded mutation declarations, and every executable write asset SHALL be digest-referenced by the plugin manifest and generated Run package.
|
||||
|
||||
#### Scenario: Declared write is dispatched
|
||||
- **WHEN** Platform dispatches a confirmed named-field command or mutation draft
|
||||
- **THEN** Run executes only the packaged asset whose plugin ID/version, adapter/schema version, action key, server binding, and digest all match the reviewed draft
|
||||
|
||||
#### Scenario: Write asset identity changed
|
||||
- **WHEN** an action key, adapter version, packaged asset, or digest no longer matches the reviewed draft
|
||||
- **THEN** Platform or Run rejects the write before any game command or database mutation executes
|
||||
|
||||
### Requirement: Plugin-owned command execution
|
||||
SCUM writes with a verified game command SHALL use plugin-owned typed command templates, and Platform and Run SHALL NOT hardcode SCUM command strings.
|
||||
|
||||
#### Scenario: Fame or currency is changed
|
||||
- **WHEN** the compatible plugin adapter declares a supported Fame, normal-currency, or gold command
|
||||
- **THEN** Platform dispatches its template key and validated parameters through the generic Run/RCON transport and performs the declared confirmation read
|
||||
|
||||
#### Scenario: Command template is not compatible
|
||||
- **WHEN** the current plugin/game adapter cannot verify the command and confirmation contract
|
||||
- **THEN** the write capability is disabled and SHALL NOT fall back to direct SQLite mutation or Platform-built command text
|
||||
|
||||
### Requirement: Named and preserving XML attribute mutation
|
||||
Character attribute writes SHALL target version-scoped named attributes or existing skill nodes in the profile XML source verified by the active current-service adapter and SHALL preserve all untargeted XML content.
|
||||
|
||||
#### Scenario: One allowed attribute changes
|
||||
- **WHEN** Run reads a valid current XML document and applies an authorized and explicitly confirmed named-field change
|
||||
- **THEN** only the targeted allowlisted attribute/node changes and unknown attributes, nodes, ordering-sensitive extensions, and all other values remain semantically intact
|
||||
|
||||
#### Scenario: Unknown field or missing skill is requested
|
||||
- **WHEN** a requested attribute/skill is absent from the version adapter or the XML lacks the targeted existing skill node
|
||||
- **THEN** Run rejects the mutation and SHALL NOT invent a node, default an `Attribute` value, or rewrite the document from an incomplete struct
|
||||
|
||||
#### Scenario: XML is malformed
|
||||
- **WHEN** the current adapter-confirmed profile XML payload cannot be parsed by the preserving patcher
|
||||
- **THEN** no database write occurs and Platform receives a bounded safe failure
|
||||
|
||||
### Requirement: `855` is an explicit preset, not a database field
|
||||
The system SHALL NOT declare or execute `855` as a table column or generic integer field; it MAY expose `855` only after an operator-confirmed preset maps it to explicit version-scoped named character attributes and values.
|
||||
|
||||
#### Scenario: Preset meaning is not confirmed
|
||||
- **WHEN** the plugin adapter has no reviewed mapping for the `855` label
|
||||
- **THEN** the preset is absent/disabled and no `fieldKey=855`, `prisoner.value`, or `0..100000` mutation can be created
|
||||
|
||||
#### Scenario: Confirmed preset is reviewed
|
||||
- **WHEN** a compatible adapter declares the preset and an authorized user selects it
|
||||
- **THEN** the review shows every named attribute's current and proposed value rather than a single opaque `855` value
|
||||
|
||||
### Requirement: Guarded single-row database mutation
|
||||
Every database/XML mutation SHALL require a current compatible probe, target identity, expected before values/checksum, reason, idempotency key, genuine same-server backup evidence, required offline/maintenance evidence, a single-row bound, and read-after-write confirmation.
|
||||
|
||||
#### Scenario: Safety evidence is missing or fabricated
|
||||
- **WHEN** the player's online state is unknown/unsafe, maintenance is unverified, the backup reference is absent/not restorable/not for the same database instance, or a timestamp-shaped placeholder is supplied
|
||||
- **THEN** Platform rejects dispatch and no mutation job is created
|
||||
|
||||
#### Scenario: Current value changed
|
||||
- **WHEN** the row identity, current value, XML digest, or schema fingerprint no longer matches the reviewed before state
|
||||
- **THEN** Run aborts the transaction as a conflict without applying the requested value
|
||||
|
||||
#### Scenario: Affected row count is not one
|
||||
- **WHEN** a mutation would affect zero or more than one row
|
||||
- **THEN** Run rolls back and reports failure
|
||||
|
||||
#### Scenario: Readback does not confirm the target values
|
||||
- **WHEN** execution may have occurred but the confirmation read is missing or mismatched
|
||||
- **THEN** Platform SHALL NOT report success and SHALL require confirmation before any explicit retry
|
||||
|
||||
#### Scenario: Attribute save completes
|
||||
- **WHEN** the single-row mutation and readback confirm every named target value and new digest
|
||||
- **THEN** Platform records the safe result and updates local verified details without exposing the raw XML
|
||||
|
||||
### Requirement: No implicit destructive activation
|
||||
Saving character attributes SHALL NOT implicitly kill, respawn, kick, or otherwise disrupt the player.
|
||||
|
||||
#### Scenario: Game requires respawn for new values
|
||||
- **WHEN** current-service verification shows a respawn/death action is necessary for activation
|
||||
- **THEN** that action is a separate explicitly named destructive command with its own permission and fresh user confirmation and is not automatically chained to attribute save
|
||||
-95
@@ -1,95 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Server-scoped gift package CRUD
|
||||
The system SHALL provide effective-user-authorized create, read, update, enable/disable, and delete operations for gift packages and typed items scoped to one server instance and compatible plugin/game version.
|
||||
|
||||
#### Scenario: User has gift read access only
|
||||
- **WHEN** the current session has `server.game-client.read` but lacks `server.game-client.maintenance`
|
||||
- **THEN** package data and delivery history remain readable while create/edit/enable/delete controls are unavailable and direct mutation requests are forbidden
|
||||
|
||||
#### Scenario: Operator creates a valid package
|
||||
- **WHEN** an operator with effective `server.game-client.maintenance` submits a name, classification, eligibility/limit rules, and bounded quantities of plugin-catalogued item keys
|
||||
- **THEN** Platform stores the package and items for the target server without storing raw RCON, shell, SQL, or arbitrary command strings
|
||||
|
||||
#### Scenario: Package item is invalid
|
||||
- **WHEN** a package contains an unknown or incompatible item key, duplicate item, out-of-range quantity, or game-version mismatch
|
||||
- **THEN** Platform rejects the change and preserves the previous package
|
||||
|
||||
#### Scenario: Package is deleted
|
||||
- **WHEN** an authorized operator confirms deletion of a package not protected by an active delivery
|
||||
- **THEN** Platform removes or retires the package without deleting immutable completed delivery facts
|
||||
|
||||
### Requirement: Plugin-owned immutable gift catalog and transport
|
||||
Gift item aliases, compatibility rules, quantities, and delivery transport declarations SHALL come only from versioned digest-referenced assets in the plugin package and, where executable, the generated Run package.
|
||||
|
||||
#### Scenario: Catalog or transport digest changed
|
||||
- **WHEN** a package or frozen delivery references a catalog/transport version or digest that no longer matches the active compatible plugin package
|
||||
- **THEN** Platform rejects creation or dispatch and SHALL NOT translate the item into an arbitrary command or newer unreviewed alias
|
||||
|
||||
### Requirement: Validated gift eligibility and limits
|
||||
The system SHALL evaluate package eligibility and atomically reserve database-backed per-player, per-server, and configured period limits in the server's declared timezone before a delivery can dispatch.
|
||||
|
||||
#### Scenario: Player belongs to another server
|
||||
- **WHEN** a caller attempts to deliver a package to a player record outside the package's server instance
|
||||
- **THEN** Platform rejects the request before creating a delivery or Run job
|
||||
|
||||
#### Scenario: Period limit is reached
|
||||
- **WHEN** a player already has the allowed number of reserved, in-progress, partial, unknown, or delivered grants in the active daily/weekly or declared period boundary
|
||||
- **THEN** Platform rejects another delivery without relying on an in-memory count
|
||||
|
||||
#### Scenario: Concurrent eligibility requests occur
|
||||
- **WHEN** two workers request the same limited package for the same player and period concurrently
|
||||
- **THEN** a server-scoped uniqueness/idempotency constraint allows at most one request to reserve the remaining entitlement
|
||||
|
||||
#### Scenario: Delivery conclusively did not execute
|
||||
- **WHEN** Run returns a conclusive failure proving that no gift effect occurred
|
||||
- **THEN** Platform may release the reserved period entitlement transactionally; partial or unknown outcomes SHALL continue to hold it until reconciled
|
||||
|
||||
### Requirement: Frozen and idempotent gift delivery
|
||||
The system SHALL freeze the target player, package/item definitions, plugin/game version, and delivery parameters before dispatch and SHALL use a stable server-scoped delivery ID and idempotency key.
|
||||
|
||||
#### Scenario: Package changes after delivery request
|
||||
- **WHEN** an operator edits or disables a package after a delivery has been created
|
||||
- **THEN** the existing delivery retains its frozen target/items/version and the edit does not mutate an in-flight or completed delivery
|
||||
|
||||
#### Scenario: Same idempotency key is submitted again
|
||||
- **WHEN** the same scoped delivery request is repeated or submitted concurrently
|
||||
- **THEN** Platform returns the original delivery and queues no duplicate effect
|
||||
|
||||
#### Scenario: Run receives a delivery
|
||||
- **WHEN** a user with current effective `server.game-client.command` confirms an eligible frozen delivery
|
||||
- **THEN** Run receives only the plugin-declared typed item aliases/quantities and stable delivery identity, not arbitrary browser commands
|
||||
|
||||
### Requirement: Conclusive delivery outcomes
|
||||
The system SHALL record a gift as delivered only after conclusive Run execution evidence, and ambiguous or partial outcomes SHALL not trigger automatic whole-package redelivery.
|
||||
|
||||
#### Scenario: Job is queued or accepted
|
||||
- **WHEN** Run has only claimed, acknowledged, or begun the delivery job
|
||||
- **THEN** Platform records the delivery as in progress and SHALL NOT create a successful claim/receipt
|
||||
|
||||
#### Scenario: Delivery succeeds conclusively
|
||||
- **WHEN** Run returns a schema-valid success receipt for every required item under the delivery idempotency key
|
||||
- **THEN** Platform records the delivery as delivered exactly once
|
||||
|
||||
#### Scenario: Connection is lost after possible execution
|
||||
- **WHEN** delivery may have executed but the result is missing, timed out, cancelled, or cannot be confirmed
|
||||
- **THEN** Platform records an unknown outcome, performs confirmation before any explicit retry, and never automatically queues the entire delivery again
|
||||
|
||||
#### Scenario: Multi-item delivery is partial
|
||||
- **WHEN** the transport cannot guarantee atomic delivery and only some item receipts are conclusive
|
||||
- **THEN** Platform records per-item receipts and a partial/unknown package outcome and SHALL NOT label the whole package delivered or blindly redeliver confirmed items
|
||||
|
||||
#### Scenario: Notification fails after delivery
|
||||
- **WHEN** all items are confirmed delivered but a post-delivery notification fails
|
||||
- **THEN** Platform preserves the delivered fact, records the notification failure separately, and SHALL NOT redeliver the package
|
||||
|
||||
### Requirement: Gift management surface and history
|
||||
The gift page SHALL provide statistics, a searchable/filterable paginated package table, package create/edit/detail/delete controls, a real player selector, a reviewed send dialog, and server-scoped delivery history without Workflow terminology.
|
||||
|
||||
#### Scenario: Operator opens gift management
|
||||
- **WHEN** local gift data is available
|
||||
- **THEN** the page shows package totals/enabled counts/delivery statistics, package contents and limits, and compact row actions without a hard-coded `starter-pack`, fixed player cards, or fixed notification text
|
||||
|
||||
#### Scenario: Operator reviews delivery result
|
||||
- **WHEN** a delivery is in progress, delivered, failed, unknown, partial, or has a notification failure
|
||||
- **THEN** the page shows the ordinary gift-delivery result and safe reason without exposing a Workflow instance, Workflow step, raw command, or audit-initiation action
|
||||
-174
@@ -1,174 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Current-service schema compatibility gate
|
||||
The system SHALL enable each SCUM database-backed capability only after the bound Run has performed a read-only probe against the current server database and the result matches a versioned plugin schema adapter for that capability.
|
||||
|
||||
#### Scenario: Supported current schema is detected
|
||||
- **WHEN** the bound Run reports a schema fingerprint whose required tables, columns, types, joins, and cardinality match the plugin adapter for player reads
|
||||
- **THEN** Platform enables the player-read capability for that server and records the adapter version, fingerprint, probe time, and safe capability result
|
||||
|
||||
#### Scenario: A current binding has no valid probe evidence
|
||||
- **WHEN** a compatible Run binding becomes ready, reconnects, or reports a changed database/plugin identity without current probe evidence
|
||||
- **THEN** Platform automatically schedules the bounded read-only probe before any affected query or write and exposes no browser action for starting it
|
||||
|
||||
#### Scenario: Generic probe execution is unavailable
|
||||
- **WHEN** the bound Run lacks the required generic diagnostic-probe contract
|
||||
- **THEN** database-backed SCUM capabilities remain disabled with a safe connection/capability reason and SHALL NOT fall back to a copied database, cached schema, reference model, or guessed query
|
||||
|
||||
#### Scenario: One world-data requirement is incompatible
|
||||
- **WHEN** a required vehicle column is missing, has an incompatible type, or the declared join is not unique while player and squad requirements still match
|
||||
- **THEN** Platform disables only the affected vehicle capability and SHALL NOT treat an empty vehicle list as a successful current-server result
|
||||
|
||||
#### Scenario: Compatibility evidence becomes invalid
|
||||
- **WHEN** the game version, database instance identity, Run binding, plugin adapter version, or schema fingerprint changes
|
||||
- **THEN** Platform invalidates the affected capability evidence and requires a new successful probe before another query or write uses that evidence
|
||||
|
||||
#### Scenario: Probe result is exposed safely
|
||||
- **WHEN** Platform stores or returns schema compatibility status
|
||||
- **THEN** the result excludes the database path, DSN, socket, credentials, raw SQL, raw row content, and host identity
|
||||
|
||||
#### Scenario: Run snapshots the active database for a safe read
|
||||
- **WHEN** Run must use an execution-time read-only snapshot to avoid locking the active SQLite database
|
||||
- **THEN** the snapshot is bound to the current server/Run/database identity, timestamped, checksummed, short-lived, and rejected after rebinding or source-identity change
|
||||
|
||||
### Requirement: Plugin-owned immutable parser and query assets
|
||||
The SCUM plugin SHALL own versioned login parsers, SQLite query assets, and parameter/result schemas, and each executable data asset SHALL be referenced by an integrity digest in the plugin manifest and generated Run package.
|
||||
|
||||
#### Scenario: Declared query is dispatched
|
||||
- **WHEN** Platform schedules the declared player query with schema-valid bounded parameters
|
||||
- **THEN** Run executes the matching packaged asset only after the plugin ID, version, template key, server binding, and asset digest all match
|
||||
|
||||
#### Scenario: Caller supplies arbitrary query material
|
||||
- **WHEN** a browser, AI request, Platform API caller, or job payload supplies raw SQL, an unknown template key, extra parameters, or a different asset digest
|
||||
- **THEN** the request is rejected before SQLite execution and no protected query content is stored in a browser-visible response
|
||||
|
||||
#### Scenario: SCUM behavior is absent from Run
|
||||
- **WHEN** Run selects an executor for a SCUM plugin query
|
||||
- **THEN** it uses generic SQLite and packaged-asset contracts and contains no branch keyed by SCUM, a `scum.*` template name, a SCUM table, or a SCUM command
|
||||
|
||||
### Requirement: Bounded and correlated SQLite reads
|
||||
Run SHALL execute plugin query assets with bound parameters and query-only restrictions, and Platform SHALL correlate every result to its original durable job before validating and storing it.
|
||||
|
||||
#### Scenario: Query violates the read boundary
|
||||
- **WHEN** an asset or parameter expansion attempts multiple statements, DDL, mutation, `ATTACH`, extension loading, a write PRAGMA, or string-concatenated parameter injection
|
||||
- **THEN** Run rejects execution with a stable safe error and returns no rows
|
||||
|
||||
#### Scenario: Query exceeds a declared bound
|
||||
- **WHEN** execution times out, the database remains locked, or rows/result bytes exceed the template limits
|
||||
- **THEN** Run stops the read and returns a bounded safe error without converting the failure into an empty successful result
|
||||
|
||||
#### Scenario: Fast, duplicate, or late result arrives
|
||||
- **WHEN** a result arrives immediately, is delivered more than once, or arrives after a newer scan
|
||||
- **THEN** the pre-existing job correlation prevents loss or cross-server association, duplicate delivery is idempotent, and an older result cannot replace a newer completed generation
|
||||
|
||||
#### Scenario: Result envelope is malformed
|
||||
- **WHEN** the server/plugin binding, template key, schema version, asset digest, checksum, observed time, or row schema does not match the queued job
|
||||
- **THEN** Platform rejects the result before any local SCUM record changes
|
||||
|
||||
### Requirement: Authentic login-driven player and session records
|
||||
The system SHALL parse the plugin-declared current SCUM login/logout format and atomically maintain a player and session uniquely scoped by server instance and external player identifier.
|
||||
|
||||
#### Scenario: Parser version is enabled
|
||||
- **WHEN** a plugin login parser is registered for a SCUM/game version
|
||||
- **THEN** sanitized fixtures captured from the active service prove its successful login/logout mapping and parser digest before it can create durable players
|
||||
|
||||
#### Scenario: Unknown player logs in successfully
|
||||
- **WHEN** an authentic successful login event contains a valid external player ID, display name, event identity, and occurrence time
|
||||
- **THEN** Platform atomically creates or updates exactly one `(server_instance_id, external_player_id)` player and opens one matching local session
|
||||
|
||||
#### Scenario: Concurrent first-login delivery occurs
|
||||
- **WHEN** two workers process the same new player's first successful login concurrently
|
||||
- **THEN** a database unique constraint and idempotent event identity produce one player and one session without a duplicate-key user-visible failure
|
||||
|
||||
#### Scenario: Duplicate or older event arrives
|
||||
- **WHEN** an acknowledged login/logout event is replayed or an older logout arrives after a later login
|
||||
- **THEN** Platform creates no duplicate session and does not regress the player's current display name, last-seen time, or newer online session
|
||||
|
||||
#### Scenario: Source format has no native session identifier
|
||||
- **WHEN** a valid login line has a transport cursor and player identity but no game-provided event/session ID
|
||||
- **THEN** Platform derives a privacy-safe logical event fingerprint and deterministic server-scoped session identity from normalized non-network event fields, independent of stream generation, so replaying that logical line cannot create a second session
|
||||
|
||||
#### Scenario: Line is malformed or login did not succeed
|
||||
- **WHEN** a partial, malformed, undecodable, or oversized line, failed authentication attempt, disconnect without a matching identity, or unrelated log message is received
|
||||
- **THEN** the parser emits no successful-login event and Platform creates no player or fabricated session
|
||||
|
||||
#### Scenario: Logout has no matching session
|
||||
- **WHEN** a valid logout event has no matching open session under the same server/player/source epoch
|
||||
- **THEN** Platform records no fabricated login session and does not close a different or newer session
|
||||
|
||||
#### Scenario: Network material is present
|
||||
- **WHEN** the source login line includes an IP address or another network identifier
|
||||
- **THEN** raw network material is discarded before durable player/session storage and is absent from SCUM APIs
|
||||
|
||||
#### Scenario: Database save time changes
|
||||
- **WHEN** a SCUM.db row reports `last_login_time`, `last_save_time`, or another persistence timestamp without a corresponding current login session event
|
||||
- **THEN** Platform SHALL NOT mark the player online from that database timestamp alone
|
||||
|
||||
### Requirement: Login stream continuity and binding fences
|
||||
The login source SHALL keep transport cursor identity separate from stable logical event identity and preserve current-binding isolation across batch replay, file rotation/truncation, Run restart, reconnect, and partial-line boundaries.
|
||||
|
||||
#### Scenario: Log file rotates or truncates
|
||||
- **WHEN** the declared login file is replaced, rotated, or truncated
|
||||
- **THEN** Run starts a new durable transport generation and resumes only at a complete-line boundary while any overlapped logical event retains the same privacy-safe fingerprint
|
||||
|
||||
#### Scenario: Rotation overlap replays an acknowledged login
|
||||
- **WHEN** a previously acknowledged logical login line is observed again under a new stream generation during copy-truncate or rotation overlap
|
||||
- **THEN** the new transport event is acknowledged idempotently and Platform creates no second player or session
|
||||
|
||||
#### Scenario: Run restarts with an acknowledged cursor
|
||||
- **WHEN** Run restarts or reconnects after Platform acknowledged a source sequence
|
||||
- **THEN** replay begins from a safe acknowledged boundary and duplicate batches remain idempotent
|
||||
|
||||
#### Scenario: Event belongs to an obsolete binding
|
||||
- **WHEN** a typed login/logout event carries a Run binding, server instance, plugin version, stream generation, or source identity that no longer matches the active binding
|
||||
- **THEN** Platform rejects it before changing a player or session
|
||||
|
||||
#### Scenario: Online session loses authoritative log continuity
|
||||
- **WHEN** the binding is replaced, the server stops, or log continuity is lost without a matching logout event
|
||||
- **THEN** Platform closes or marks the affected session unknown with a bounded terminal reason and SHALL NOT continue presenting it as a confirmed current login
|
||||
|
||||
### Requirement: Automatic server-bound synchronization
|
||||
The system SHALL synchronize verified SCUM.db resources automatically when a compatible Run binding becomes ready and at plugin-declared bounded cadences, without a user-facing manual data-refresh operation.
|
||||
|
||||
#### Scenario: Compatible Run becomes ready
|
||||
- **WHEN** a server obtains a current Run binding and successful capability probe
|
||||
- **THEN** Platform schedules an initial bounded scan and later jittered resource scans within per-server concurrency limits
|
||||
|
||||
#### Scenario: New login needs enrichment
|
||||
- **WHEN** login ingestion creates a player whose database details are not yet known
|
||||
- **THEN** Platform may schedule a bounded player-detail lookup without delaying player creation or filling unknown values with defaults
|
||||
|
||||
#### Scenario: Page is opened
|
||||
- **WHEN** an operator opens users, squads, map, or gifts
|
||||
- **THEN** the page reads the platform-local data API and SHALL NOT dispatch a Run query, schema probe, projection refresh, audit, or synchronization command
|
||||
|
||||
#### Scenario: A later scan fails
|
||||
- **WHEN** a resource previously synchronized successfully and a later scan fails or is partial
|
||||
- **THEN** Platform retains the last completed local generation, records the safe connection/sync failure internally, and does not delete rows or present generated replacements
|
||||
|
||||
#### Scenario: Resource synchronization status is requested
|
||||
- **WHEN** a local resource API reports its availability
|
||||
- **THEN** it distinguishes not-yet-synchronized, successfully-synchronized-empty, schema-incompatible, current/last-complete data, and connection/read failure with concrete collection time and a safe reason
|
||||
|
||||
#### Scenario: No completed local generation exists
|
||||
- **WHEN** a current scan has not completed successfully
|
||||
- **THEN** Platform returns the appropriate unavailable/not-yet-synchronized state and SHALL NOT fall back to a legacy projection, sample record, reference database, cached unbound row, or fabricated empty success
|
||||
|
||||
### Requirement: Dedicated local SCUM database records
|
||||
The system SHALL persist SCUM identities, sessions, details, squads, squad members, vehicles, flags, positions, gifts, deliveries, and sync cursors in dedicated queryable local stores rather than in SCUM projection/Workflow snapshot fields.
|
||||
|
||||
#### Scenario: File storage backend is active
|
||||
- **WHEN** Platform runs with the default file metadata backend
|
||||
- **THEN** SCUM records use a platform-owned local database under the configured data directory and a position update does not rewrite the global metadata snapshot
|
||||
|
||||
#### Scenario: MySQL storage backend is active
|
||||
- **WHEN** Platform runs with the MySQL backend
|
||||
- **THEN** explicit migrations create normalized SCUM tables with server-scoped indexes and uniqueness constraints instead of storing SCUM collections only inside `platform_metadata_snapshots.snapshot_json`
|
||||
|
||||
#### Scenario: Source omits a numeric fact
|
||||
- **WHEN** a verified row omits fame, currency, a coordinate, an owner, or another fact
|
||||
- **THEN** the corresponding local field remains null/absent and SHALL NOT be set to zero, an inferred owner, a generated coordinate, or an unrelated identifier
|
||||
|
||||
#### Scenario: Full scan completes
|
||||
- **WHEN** every page of a bounded full resource scan passes schema and checksum validation
|
||||
- **THEN** Platform transactionally commits that generation and may mark records absent only from that completed generation
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Lean SCUM player record
|
||||
The SCUM product SHALL maintain only server-scoped player identity, current display name, first/last seen times, online session/login history, and verified current-server player details required for management.
|
||||
|
||||
#### Scenario: Player changes display name
|
||||
- **WHEN** a newer authentic login event reports a different display name for an existing server-scoped player
|
||||
- **THEN** Platform updates the current display name and SHALL NOT create alias-intelligence, shared-IP, access-risk, or automatic-enforcement records
|
||||
|
||||
#### Scenario: SCUM player model is migrated
|
||||
- **WHEN** the new player store becomes authoritative
|
||||
- **THEN** SCUM page/API/manifest/log-ingestion flows no longer create, update, read, or expose player-intelligence, access-attempt, shared-fingerprint, security-signal, alias-history, projection, observation, or Workflow records; a shared non-SCUM domain may remain only for a proven non-SCUM consumer
|
||||
|
||||
### Requirement: Searchable and paginated player roster
|
||||
The system SHALL provide a server-authorized local player-list API and full-width table with server-side pagination, bounded search, online-state and squad filters, and allowlisted sorting.
|
||||
|
||||
#### Scenario: Operator searches the roster
|
||||
- **WHEN** an authorized operator searches by display name or external player ID and selects online or squad filters
|
||||
- **THEN** Platform returns only matching players from the target server with total/page information and deterministic ordering
|
||||
|
||||
#### Scenario: Cross-server player identifier is supplied
|
||||
- **WHEN** the same external player ID exists on another server or a filter references another server's squad
|
||||
- **THEN** the response includes no cross-server row and reveals no other server association
|
||||
|
||||
#### Scenario: Unauthorized user requests players
|
||||
- **WHEN** the session lacks effective read access to the target server
|
||||
- **THEN** Platform denies the request without revealing whether any player exists
|
||||
|
||||
### Requirement: Trustworthy roster values
|
||||
The player roster SHALL display current stored business fields without fabricating defaults and SHALL distinguish online-session evidence from database details.
|
||||
|
||||
#### Scenario: Verified player details exist
|
||||
- **WHEN** local records contain verified fame, cash, gold, squad, and last activity facts
|
||||
- **THEN** the roster shows player name, unique identifier, online state, those verified facts, and their last synchronized time
|
||||
|
||||
#### Scenario: Database details are not yet known
|
||||
- **WHEN** a login-created player has no successful detail row yet
|
||||
- **THEN** the roster shows the player identity/session with unknown detail fields and SHALL NOT substitute zero, `1`, sample data, or a guessed profile identifier
|
||||
|
||||
#### Scenario: No players have been recorded
|
||||
- **WHEN** the local player table is confirmed empty
|
||||
- **THEN** the page shows a normal player-data empty state without projection/Workflow terminology, sample players, or an action that starts data collection
|
||||
|
||||
### Requirement: Player detail and login history
|
||||
The system SHALL provide a player detail surface containing server-scoped identity, current verified facts, current known coordinate, login history, and last synchronized/collected times.
|
||||
|
||||
#### Scenario: Operator opens player detail
|
||||
- **WHEN** an authorized operator selects a roster row
|
||||
- **THEN** a drawer, dialog, or detail route shows the player's known fields and bounded login history without exposing raw logs, IP addresses, database rows, SQL, XML, paths, or credentials
|
||||
|
||||
#### Scenario: Coordinate is unavailable
|
||||
- **WHEN** no verified current coordinate exists for the player
|
||||
- **THEN** the detail surface states that the coordinate is unavailable and SHALL NOT calculate or render a default map point
|
||||
|
||||
### Requirement: Explicit player edit form
|
||||
The user-management surface SHALL edit player values through a named-field form that shows current and proposed values and delegates execution to the controlled-write capability.
|
||||
|
||||
#### Scenario: Operator edits an economy field
|
||||
- **WHEN** an operator opens the Fame, cash, or gold edit control
|
||||
- **THEN** the form requires an explicit target value and reason and SHALL NOT use hard-coded `+100`, `+1000`, or other fixed increment behavior
|
||||
|
||||
#### Scenario: Attribute current value is unknown
|
||||
- **WHEN** an operator opens an attribute or `855` preset edit without verified current named values
|
||||
- **THEN** the UI prevents submission and explains that current values must be known; it SHALL NOT default the before or after value to `0` or `1`
|
||||
|
||||
#### Scenario: User has read but not write access
|
||||
- **WHEN** the current session may read players but lacks the effective permission for the selected field
|
||||
- **THEN** the roster and detail remain available while the edit action is hidden or disabled with a readable permission explanation
|
||||
-95
@@ -1,95 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Exact SCUM detail navigation
|
||||
The SCUM server detail view SHALL contain exactly five tabs in this order: `用户管理`, `队伍管理`, `实时地图`, `礼包管理`, `AI 助手`.
|
||||
|
||||
#### Scenario: SCUM detail opens normally
|
||||
- **WHEN** an operator opens a SCUM server detail without a valid section selector
|
||||
- **THEN** the page selects `用户管理` as the default and renders no standalone `管理` or `Workflow 状态` tab
|
||||
|
||||
#### Scenario: Legacy section is requested
|
||||
- **WHEN** a legacy `manage`, `workflows`, or otherwise invalid SCUM section is requested
|
||||
- **THEN** the page replaces/falls back to `用户管理` without an empty screen or a Workflow/management placeholder
|
||||
|
||||
#### Scenario: Non-SCUM server detail is rendered
|
||||
- **WHEN** another plugin/server type legitimately declares different navigation
|
||||
- **THEN** the SCUM-only five-tab restriction does not silently remove that plugin's declared pages
|
||||
|
||||
### Requirement: Required management controls are relocated
|
||||
Removing the SCUM `管理` tab SHALL NOT remove server deployment, metadata, or administrator-membership capabilities.
|
||||
|
||||
#### Scenario: Operator edits deployment
|
||||
- **WHEN** an authorized operator needs to change the SCUM deployment definition
|
||||
- **THEN** the existing server-list deployment action remains available and no replacement permanent detail tab is introduced
|
||||
|
||||
#### Scenario: Server owner opens settings
|
||||
- **WHEN** the server owner selects the compact settings action in the detail header
|
||||
- **THEN** a drawer or dialog permits display-name and administrator membership changes with the existing ownership authorization
|
||||
|
||||
#### Scenario: Non-owner opens server settings
|
||||
- **WHEN** a server member who is not the owner views the settings surface
|
||||
- **THEN** owner-only membership mutations are unavailable and a direct API attempt remains forbidden
|
||||
|
||||
### Requirement: Projection and Workflow concepts are removed from the product
|
||||
SCUM pages and browser contracts SHALL NOT render or offer projection, observation, manual synchronization, audit-initiation, pending-review counter, or Workflow product concepts.
|
||||
|
||||
#### Scenario: Any retained SCUM page renders
|
||||
- **WHEN** users, squads, map, gifts, or AI content is displayed
|
||||
- **THEN** it contains none of `Workflow 状态`, `投影`, `真实投影`, `玩家投影`, `刷新投影`, `刷新世界投影`, `刷新真实数据`, `发起审计`, `创建发放 workflow`, `typed workflow`, `typed observation`, `typed operation`, `待审操作`, `审批/确认队列`, `清理旧入口`, `目前暂无真实投影数据`, `暂无真实投影数据`, `暂无玩家投影`, or `Companion 可用`
|
||||
|
||||
#### Scenario: Plugin page loads data
|
||||
- **WHEN** any of the four SCUM management pages initializes or retries a failed local read
|
||||
- **THEN** it calls only its local resource API and does not list/create Workflow instances/steps or start a query, projection refresh, real-data refresh, or audit
|
||||
|
||||
#### Scenario: Removed endpoint is requested
|
||||
- **WHEN** a browser requests the removed SCUM Workflow, Workflow-step, observation, operation-list/approve, projection/real-data refresh, or audit-initiation endpoint
|
||||
- **THEN** Platform returns not found or a stable removal response without dispatching a job or exposing a replacement raw execution path
|
||||
|
||||
#### Scenario: Legacy operation approval route is requested
|
||||
- **WHEN** a caller invokes a legacy SCUM operation-list, `/scum/operations/{id}/approve`, pending-review, or confirmation-queue route
|
||||
- **THEN** Platform returns not found or a stable removal response and SHALL NOT create an approval record, dispatch a write, or revive an approval product domain
|
||||
|
||||
#### Scenario: Internal write safety is recorded
|
||||
- **WHEN** Platform records authorization, idempotency, backup, execution, or confirmation evidence for a write
|
||||
- **THEN** the record remains internal/safe and does not create a user-facing audit-start action or Workflow status page
|
||||
|
||||
### Requirement: Trustworthy management page layouts
|
||||
The user, squad, map, and gift pages SHALL use full-width business data layouts and shared theme-aware table, drawer, dialog, and status patterns without placeholder product data.
|
||||
|
||||
#### Scenario: Management page is implemented
|
||||
- **WHEN** a first-party SCUM page adds or changes styles
|
||||
- **THEN** it preserves black-mecha and magical-girl theme readability, reuses shared tokens/components, keeps CSS declarations compact, and does not add unrelated opaque SaaS cards or page-local fixed decorative effects
|
||||
|
||||
#### Scenario: Data load fails
|
||||
- **WHEN** a local API request fails
|
||||
- **THEN** the page shows a readable error and may retry only the local read; it does not generate sample players, squads, vehicles, flags, positions, gifts, maintenance evidence, backups, or operation results
|
||||
|
||||
### Requirement: AI assistant is preserved with reviewable effects
|
||||
The `AI 助手` SHALL remain the final SCUM tab and SHALL support plugin-declared configuration suggestions and controlled player-operation drafts without receiving provider secrets or independent write authority.
|
||||
|
||||
#### Scenario: AI suggests plugin configuration
|
||||
- **WHEN** an authorized operator requests SCUM plugin configuration assistance
|
||||
- **THEN** AI proposes only plugin-declared fields, Platform validates and displays a reviewable diff, and the authorized review-and-confirm config-write path applies the change only after explicit confirmation
|
||||
|
||||
#### Scenario: User can view but cannot apply a suggestion
|
||||
- **WHEN** the user may invoke/read an AI suggestion but lacks the permission required for the proposed config or player write
|
||||
- **THEN** the suggestion remains reviewable while the apply action is disabled and backend application is forbidden
|
||||
|
||||
#### Scenario: AI provider is invoked
|
||||
- **WHEN** Platform sends the allowed prompt/context to the configured AI provider
|
||||
- **THEN** raw AI keys, database credentials, SCUM.db paths, raw XML, and protected Run material never reach the plugin page/browser
|
||||
|
||||
### Requirement: Obsolete UI actions and placeholders are removed
|
||||
The SCUM product SHALL replace hard-coded actions and placeholder map/gift behavior with the declared business flows.
|
||||
|
||||
#### Scenario: Player actions render
|
||||
- **WHEN** a player has writable verified fields
|
||||
- **THEN** the UI offers explicit named-field editing and contains no `Fame +100`, `现金 +1000`, `855 审批`, fake before value, fake maintenance ID, or fake backup reference
|
||||
|
||||
#### Scenario: Map renders
|
||||
- **WHEN** the map page is available
|
||||
- **THEN** it uses the verified map capability and contains no gradient-only board or arbitrary percentage-dot transform
|
||||
|
||||
#### Scenario: Gift page renders
|
||||
- **WHEN** gift management is available
|
||||
- **THEN** it reads local gift packages and contains no hard-coded `starter-pack`, fixed player-card delivery, or fixed notification text
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Independently verified current map facts
|
||||
The system SHALL store and present current player, vehicle, flag, and squad-territory coordinates only when each resource's plugin adapter maps a compatible current-service source, identity, coordinate, and observation time.
|
||||
|
||||
#### Scenario: One position resource is incompatible
|
||||
- **WHEN** player and flag positions are compatible but the vehicle query lacks a required column, type, identity, or unique join
|
||||
- **THEN** Platform keeps the verified player/flag layers available, disables only the vehicle layer, and SHALL NOT return an empty vehicle list as a successful current result
|
||||
|
||||
#### Scenario: Vehicle ownership is unverified
|
||||
- **WHEN** the source proves a vehicle identity/class and coordinate but does not prove a player or squad owner
|
||||
- **THEN** Platform stores the verified fields and leaves ownership null instead of inferring it from nearby players, movement history, or an expiry heuristic
|
||||
|
||||
#### Scenario: Position scan later fails
|
||||
- **WHEN** verified positions were previously stored and a later current-service scan fails or is partial
|
||||
- **THEN** the map may retain the last completed rows with their concrete collection time and connection interruption, but SHALL NOT call them current or replace them with empty/generated points
|
||||
|
||||
### Requirement: Declared SCUM map asset and coordinate transform
|
||||
The realtime map SHALL use a plugin-declared distributable map asset/version and integrity digest, verified world bounds, and a tested coordinate transform version/digest compatible with the current SCUM adapter.
|
||||
|
||||
#### Scenario: Verified coordinate is rendered
|
||||
- **WHEN** a player, vehicle, flag, or territory coordinate falls within the declared bounds and transform version
|
||||
- **THEN** the map places the marker at the tested map position and shows object type, name/ID, source coordinate, and collection time
|
||||
|
||||
#### Scenario: Map metadata is incompatible or absent
|
||||
- **WHEN** the map asset, version, bounds, or transform does not match the current server adapter
|
||||
- **THEN** the map shows a readable unavailable/error state and SHALL NOT fall back to a gradient board, arbitrary percentage transform, fake points, or sample routes
|
||||
|
||||
#### Scenario: Map asset digest does not match
|
||||
- **WHEN** the packaged/browser-served map asset or transform contract fails its plugin-declared integrity check
|
||||
- **THEN** the map is unavailable and SHALL NOT render coordinates against an unverified replacement asset
|
||||
|
||||
#### Scenario: Coordinate is outside bounds
|
||||
- **WHEN** a source coordinate is non-finite or outside the declared world bounds
|
||||
- **THEN** Platform rejects it from the current map dataset and records a bounded validation failure
|
||||
|
||||
### Requirement: Automatically updated and distinguishable map layers
|
||||
The realtime map SHALL consume platform-local data automatically and provide distinguishable player, vehicle, and flag/squad-territory layers with player/squad filtering.
|
||||
|
||||
#### Scenario: Operator changes layer visibility
|
||||
- **WHEN** an operator toggles players, vehicles, or flags/territory or applies a player/squad filter
|
||||
- **THEN** the map updates the visible verified markers without dispatching a Run query or manual refresh operation
|
||||
|
||||
#### Scenario: Platform receives a newer coordinate
|
||||
- **WHEN** a verified automatic sync or declared companion position event updates a current position
|
||||
- **THEN** Platform publishes a safe local update and the open map can update through the platform event stream
|
||||
|
||||
#### Scenario: Source cadence is insufficient
|
||||
- **WHEN** measured SCUM.db updates cannot support the declared realtime threshold
|
||||
- **THEN** the plugin uses a declared verified companion position source or the feature presents the measured cadence honestly; it SHALL NOT increase unsafe polling or fabricate intermediate movement
|
||||
|
||||
#### Scenario: Map legend is rendered
|
||||
- **WHEN** multiple entity types are visible
|
||||
- **THEN** the UI provides icons or textual legend labels so entity meaning does not rely only on color
|
||||
|
||||
#### Scenario: Historical movement was not requested
|
||||
- **WHEN** only current positions are available under this change
|
||||
- **THEN** the product SHALL NOT fabricate trajectories, 24-hour trails, ride associations, sampling history, or playback controls to satisfy realtime-map acceptance
|
||||
|
||||
### Requirement: Server-isolated realtime-map API
|
||||
The realtime-map API SHALL authorize the target server and return only bounded local current-position and safe object-summary fields needed by the map.
|
||||
|
||||
#### Scenario: Foreign map selector is supplied
|
||||
- **WHEN** a player, squad, vehicle, flag, or territory selector belongs only to another server
|
||||
- **THEN** Platform returns no foreign marker and does not reveal its existence or server identity
|
||||
|
||||
#### Scenario: Browser requests protected source material
|
||||
- **WHEN** a browser attempts to request raw database rows, SQL, host paths, database identifiers, map-source credentials, or Run connection details
|
||||
- **THEN** the API rejects the request and returns only named safe map fields
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Independently gated squad, member, flag, and territory facts
|
||||
The system SHALL store and present squads, squad members, leader/rank meaning, flags, territories, and their relationships only from current-service rows validated by the matching per-resource plugin adapter capability.
|
||||
|
||||
#### Scenario: Squad and member adapters are compatible
|
||||
- **WHEN** squad and member queries complete successfully with verified identifiers, ranks, and player/profile relationships
|
||||
- **THEN** Platform transactionally stores server-scoped squads and members and exposes only the leader/rank meaning declared by that adapter
|
||||
|
||||
#### Scenario: One squad resource is incompatible
|
||||
- **WHEN** squad/member queries are compatible but flag ownership or territory coordinates are not
|
||||
- **THEN** Platform keeps the verified squad/member capability available, disables only the incompatible resource, and SHALL NOT treat its absence as a verified empty list
|
||||
|
||||
#### Scenario: Leader meaning is unverified
|
||||
- **WHEN** the current schema contains a rank value but the adapter cannot prove which value represents leader
|
||||
- **THEN** Platform keeps leader unknown and SHALL NOT assume that a reference-project constant such as rank `4` is authoritative
|
||||
|
||||
#### Scenario: Flag ownership join is ambiguous
|
||||
- **WHEN** a flag/base/profile relationship yields multiple possible squad owners or no verified squad link
|
||||
- **THEN** Platform records no squad owner for that flag and exposes no inferred confidence label as fact
|
||||
|
||||
### Requirement: Searchable and paginated squad roster
|
||||
The system SHALL provide a server-authorized local squad-list API and full-width table with bounded name/identifier search, server-side pagination, allowlisted sorting, and deterministic results.
|
||||
|
||||
#### Scenario: Operator searches squads
|
||||
- **WHEN** an authorized operator searches or pages the target server's squads
|
||||
- **THEN** Platform returns only matching server-local rows with total/page information, and the table shows squad ID, name, verified leader, member count, flag/territory status, known coordinate, and collection time
|
||||
|
||||
#### Scenario: No completed squad generation exists
|
||||
- **WHEN** squads have not synchronized successfully or the squad adapter is incompatible
|
||||
- **THEN** the page shows the corresponding not-yet-available/error state and SHALL NOT render reference, sample, projection, or generated squads
|
||||
|
||||
### Requirement: Semantic squad detail
|
||||
The squad surface SHALL provide a drawer, dialog, or detail route that separates leader, members/ranks, flags, and territory instead of merging unrelated rows into one list.
|
||||
|
||||
#### Scenario: Operator opens a squad
|
||||
- **WHEN** a server-authorized operator selects a squad
|
||||
- **THEN** the detail shows only verified leader/member/rank and flag/territory facts with collection times and nullable unknown fields
|
||||
|
||||
#### Scenario: Verified territory coordinate is selected
|
||||
- **WHEN** a squad or flag has a compatible verified coordinate and the realtime-map capability is available
|
||||
- **THEN** the detail may navigate to that coordinate on the map without dispatching a new Run query or inventing a fallback point
|
||||
|
||||
### Requirement: Server-isolated squad APIs
|
||||
Squad APIs SHALL authorize the target server and return only bounded local squad, member, flag, and territory fields needed by the management surface.
|
||||
|
||||
#### Scenario: Foreign selector is supplied
|
||||
- **WHEN** a squad, player/member, flag, or territory selector belongs only to another server
|
||||
- **THEN** Platform returns no foreign record and does not reveal its existence or server identity
|
||||
|
||||
#### Scenario: Browser requests protected source material
|
||||
- **WHEN** a browser attempts to request raw SCUM.db rows, query text, host paths, database identifiers, or Run connection details
|
||||
- **THEN** the API rejects the request and returns only named safe resource fields
|
||||
@@ -1,278 +0,0 @@
|
||||
## 1. Prompt Boundaries
|
||||
|
||||
- [x] 1.1 正向提示词 (Positive prompt): Rebuild the SCUM portion of the first-party `服务器管理` area around authentic current-server login events, verified current-service database facts, dedicated local records, trustworthy user/squad/map/gift management, and a preserved AI assistant. Success requires exactly five SCUM tabs, automatic player creation and synchronization, no fabricated values, and permission-checked reviewable writes.
|
||||
- [x] 1.2 方向提示词 (Directional prompt): Work in `platform/`, `platform_web/`, `plugins/`, and explicit browser-repository protocol contracts while coordinating a separately authorized task in the independent `git@git.npc0.com:admin343/run.git` repository. Use `/Users/tasia/Desktop/code/go/scum/scum_robot` only as a read-only behavioral reference for login ingestion and user/squad/map/gift interactions; derive schema, joins, commands, coordinates, and mutation semantics exclusively from the active bound service. Preserve durable jobs, channel isolation, plugin ownership, API/type directory boundaries, and the black-mecha/magical-girl visual system. Required evidence includes Platform/Plugins/Web and SCUM companion tests, the external Run task's own tests/commit/deployment evidence, current-service and browser acceptance, `scripts/check-structure.sh`, and strict OpenSpec validation.
|
||||
- [x] 1.3 任务边界 (Boundary prompt): Do not add a `run/` source tree to this repository; modify `/Users/tasia/Desktop/code/go/scum/scum_robot`; edit the independent Run repository except inside its own separately authorized/rooted task; touch unrelated roots; use an unbound repository/reference/cached `SCUM.db` as current-service evidence; trust generated/reference structs as production schema; expose raw SQL/RCON/XML/paths/credentials/sockets to browser or AI; add billing/cloud-host/agent-provider workflows; fabricate players/world facts/backups/results; create a platform-admin approval queue; or reintroduce projection, observation, audit-initiation, manual-refresh, pending-review, or Workflow product concepts. A short-lived read-only snapshot created by Run is allowed only when fenced to the active binding/database identity, timestamped, checksummed, and invalidated on source change.
|
||||
- [x] 1.4 Before implementation, confirm the browser repository is on `main` and record existing dirty files; if the branch is not `main` or local changes block a safe switch, stop without creating another branch or editing files.
|
||||
|
||||
## 2. Minimal Run Probe and Current-Service Evidence Gate
|
||||
|
||||
### External Run evidence (2026-08-12)
|
||||
|
||||
- The separately rooted Run task implemented and pushed generic schema-probe support at `git@git.npc0.com:admin343/run.git` commit `6cb6ba3` (`add bounded sqlite schema probe`); its focused protocol/runtime tests and `go test ./...` passed.
|
||||
- The executor advertises `remote.run.db.sqlite.probe`, accepts only package-scoped logical `databases/...` SQLite targets, applies query-only fixed introspection plus binding/job/fence and output bounds, and returns SHA-256-fingerprinted redacted envelopes without SCUM-specific branches or raw database content.
|
||||
- The active binding `server-run-server-scum-1785923898033` on `枣庄服务器` has been updated and reports `remote.run.db.sqlite.probe`; the endpoint was observed online through the Platform API at `2026-08-12T08:21:27Z` with 28 capabilities including the schema-probe capability.
|
||||
- A follow-up separately rooted Run task fixed logical SQLite data-target materialization at commit `8fe6f9b` (`Fix SQLite probe data target mapping`); focused runtime/data-target tests and `go test ./...` passed in the independent Run repository before deployment.
|
||||
- The fixed Run distribution `run-dist-server-scum-1785923898033-windows-amd64-1-zao-zhuang-data-target-run-fix-2026081-6988495348508259730` with checksum `sha256:a5ac9fe31ed0e0f595e70e3d3322f44aa81bd165183530e4be6939aff81c3016` was installed on `枣庄服务器`, and the active endpoint advertises both `remote.run.db.sqlite.probe` and `remote.run.db.sqlite.query`.
|
||||
|
||||
### Server-management diagnostic evidence (2026-08-12)
|
||||
|
||||
- Per operator direction, the Run install target is `枣庄服务器` (`FyBDIohqPhRx7Cia`); personal server-management MCP inventory and `test_connection` both succeeded for that device.
|
||||
- Bounded SSH diagnostics checked only process/service/capability metadata and emitted no raw SQL, database content, credentials, SCUM rows, or database reads. The server has a Windows Run process for `server-run-server-scum-1785923898033`, the current SCUM server process is running, and `https://scum.npc0.com/healthz` returned `200` from the server side; per operator clarification, `scum.npc0.com` is the NAT entry back to the local Platform.
|
||||
- Platform durable probe job `job-remote-adapter-server-scum-1785923898033-3442596095552254276` was queued through `POST /api/v1/server-instances/server-scum-1785923898033/scum/schema-probe` with idempotency key `zao-zhuang-schema-probe-20260812-1632`, claimed by the authenticated active Run binding, acknowledged, and executed with target `databases/scum-database`, `MaxAttempts=1`, and a nonzero fencing token.
|
||||
- The probe terminal result was accepted by Platform as a typed `sqlite.schema-probe` result with safe status `failed`, safe error code `target_unavailable`, result digest `sha256:41624741855866ce10b3143edba66c3a6b771029256b9489a30f395885526b61`, and observed time `2026-08-12T08:31:45Z`. This proves the Platform durable job path and active Run probe executor are wired, but it does not prove current SCUM schema compatibility.
|
||||
- The first generated Run workspace contained lifecycle package assets but no `databases/scum-database` logical database target, so current-service schema capture was initially blocked at the package/database-target mapping layer. That failure stayed closed and kept database-backed SCUM read/write gates disabled until the later Run data-target fix produced successful schema metadata.
|
||||
- Follow-up bounded SSH diagnostics on `2026-08-12` located exactly one active `SCUM.db` candidate by process-relative metadata only, with no SQL execution or row reads; the live file was locked for direct hashing/copying. This supports the package-target diagnosis but is not current-service schema evidence for tasks 2.5-2.9.
|
||||
- Platform/plugin contracts now declare a plugin-owned `runtimeProfiles.dataTargets` sqlite snapshot target for `scum-database` that materializes to `databases/scum-database` inside the generated Run workspace; SCUM schema-probe dispatch fails closed when that data target is absent. The later independent Run materializer fix supplied the matching generic data-target behavior required for the successful durable probe.
|
||||
- Operator clarification on `2026-08-12` narrowed the architecture boundary: Platform/plugin must not download or parse `SCUM.db`, but an operator-directed, server-local Python diagnostic on `枣庄服务器` is acceptable discovery evidence when it is read-only, bounded, redacted, and not treated as the product execution path. The diagnostic captured schema metadata in place and is recorded in `evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md`; database-backed product gates remain disabled until durable Run envelopes and versioned adapters are accepted.
|
||||
- After the Run data-target fix and Platform `sourceFingerprint` decode fix, durable probe job `job-remote-adapter-server-scum-1785923898033-7249327407638289501` succeeded through the product path. Platform persisted probe status `succeeded`, source fingerprint `sha256:d8f3e2f5e9c8241f55b931008309a7ab5f241118a82cbd3620ddedf233e74c13`, schema fingerprint `sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0`, result digest `sha256:ef13678df4add731c758bba157627dc8af80138a69476facd81bbe354c31d7f1`, `161` schema objects, observed time `2026-08-12T12:17:39.0088015Z`, and terminal time `2026-08-12T12:29:24.857542Z`.
|
||||
- Server-management MCP verification on `2026-08-12` confirmed `test_connection` succeeded for `枣庄服务器`, `SCUMServer.exe` was running, and the target durable probe job had `0` active entries and `0` pending-result entries in the remote Run journal after Platform accepted the typed result. The redacted evidence is stored in `evidence/scum-durable-run-schema-probe-2026-08-12.md`.
|
||||
- Follow-up server-local read-only Python probes on `2026-08-12` verified the actual current-service joins and nullable fields for external identity, profile/prisoner/entity relationships, squad members, flag/base ownership candidates, vehicle identity, bank-account balances, and XML payload candidates. The results are recorded in `evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md`; unproven rank leader semantics, squad-territory ownership, currency labels/units, command confirmation, and `855` mapping remain gated rather than guessed.
|
||||
- A task 2.8 confirmation pass on `2026-08-13` separately checked the SCUM live-data manifest, platform/plugin declarations, and read-only `scum_robot` reference behavior for economy commands, gift aliases/transports, map asset/transform authorization, and `855`. No economy command, gift catalog/transport, distributable map asset/transform, or `855` preset mapping is verified enough to enable; the old bridge/domain/reference declarations remain hypothesis or legacy facade material only, and the affected capabilities stay disabled until digest-referenced current-service adapter evidence exists.
|
||||
|
||||
- [x] 2.1 Add a release gate that keeps every database-backed SCUM read and write capability disabled until capability-specific current-service evidence matches a versioned plugin adapter; do not add production SQL or mutation assets before this group is complete.
|
||||
- [x] 2.2 Define the minimal generic schema-probe request/result contract, safe error model, binding identity, bounds, and redacted evidence DTO needed by Platform and the plugin without embedding SCUM table names or host paths in Run-facing generic code.
|
||||
- [x] 2.3 If the active binding lacks the minimal bounded query-only probe executor, create/hand off a separately authorized task rooted in the independent Run repository, wait for its tests/commit/deployment evidence, and record that evidence here; do not edit or vendor Run source from this change.
|
||||
- [x] 2.4 Use the personal server-management MCP (`list_devices`, `test_connection`, then `ssh_exec` only when needed) for device inventory, connectivity checks, and bounded diagnostics. Execute product/acceptance schema probes only as Platform durable jobs through the active authenticated Run binding; an operator-directed server-local Python diagnostic may inspect the active database in place for discovery but must not become a Platform/plugin/browser data path.
|
||||
- [x] 2.5 Capture `sqlite_master`, applicable read-only PRAGMA metadata, indexes, foreign keys, declared types, cardinalities, and small redacted samples for candidate player, profile/entity, squad/member, vehicle, flag/base, economy, coordinate, and character-profile payload sources.
|
||||
- [x] 2.6 Verify actual joins and meanings for external player identity, profiles/entities, squad ranks/leaders, flag ownership, vehicle identity, currency units/types, nullable fields, and the real table/column containing character XML; do not assume that `user_profile.template_xml` or any reference-project field exists.
|
||||
- [x] 2.7 Measure coordinate ranges and update cadence, query latency, lock/busy behavior, snapshot consistency, safe timeout/row limits, and whether a verified companion position source is needed for the advertised realtime-map cadence.
|
||||
- [x] 2.8 Confirm separately which economy commands support safe confirmation, which gift item aliases/transports are real, which distributable map asset/transform is authorized, and what named attributes—if any—the operator means by the `855` preset.
|
||||
- [x] 2.9 Store sanitized probe evidence or an immutable referenced test artifact and derive the observed schema fingerprint/evidence matrix; do not claim final adapter compatibility until the versioned adapters and query contracts in group 3 exist.
|
||||
|
||||
## 3. Plugin SDK, Manifest, and Immutable SCUM Assets
|
||||
|
||||
### Login-log fixture evidence (2026-08-13)
|
||||
|
||||
- Server-management MCP `list_devices`/`test_connection` confirmed `枣庄服务器` (`FyBDIohqPhRx7Cia`) was reachable, and bounded `ssh_exec` diagnostics observed the active `SCUMServer.exe` process without returning host paths or raw protected values.
|
||||
- Recursive active-service log discovery found `30` `login_{date}{digits}.log` files under the current service log root. The newest active login log fingerprint is `sha256:752c3ee3789fe73b80245dfcb97776db27f977c4350c3b50516278afbecb9dad`, generation `sha256:57b5be4818757e4d64070e5e0f026dbd471bdba189d0426a38b618423f7e1439`, `0` bytes, last written `2026-08-12T01:18:15.6090680Z`; Run tailing must handle zero-byte active files and later append/rotation boundaries.
|
||||
- Authentic non-empty login fixtures are UTF-16LE and match `{timestamp}: '{network_redacted} {external_player_id}:{display_name}({profile_local_id})' logged {in|out} at: X={coordinate} Y={coordinate} Z={coordinate}`. Sanitized fixture rows bind expected `scum.login`/`scum.logout` events to server `server-scum-1785923898033`, Run binding `server-run-server-scum-1785923898033`, plugin `game.scum` `0.1.6`, pending parser key `scum-login-log-parser.pending-real-fixture-v1`, parser version `pending-scum-login-log-v1`, parser digest `sha256:5bb528cb9f6e04d8d8b819db3a71f855569c78a5135f22a982301301ae1da50a`, transport cursor `(sourceIdentity, streamGeneration, sequence)`, and separate privacy-safe logical event identities. Evidence is stored in `evidence/scum-login-log-fixtures-2026-08-13.md`.
|
||||
|
||||
### Login-log parser implementation evidence (2026-08-13)
|
||||
|
||||
- Added plugin-owned parser asset `assets/scum-live/login-log-parser.json` and manifest declarations for `scum.login`/`scum.logout` under `scumLiveData.logParsers`, digest-referenced as `sha256:264835fb36255071fed46dd50724ec511986db901ac8056b08ddafb10f5f0056` while keeping database/write capability gates disabled.
|
||||
- Implemented versioned companion parser `scum-login-log-parser-v1` / `scum-login-log-v1` for UTF-16LE `login_{date}{digits}.log` lines with transport cursor `(sourceIdentity, streamGeneration, sequence)` and a privacy-safe logical identity that excludes network material, coordinates, source identity, stream generation, and sequence.
|
||||
- Added focused companion tests for successful login/logout, failed login, partial, undecodable, oversized, malformed lines, rotation/copy-truncate overlap across a new generation, restart/resume acknowledgements, duplicate transport/logical delivery, out-of-order delivery, and absence of network/coordinate material in parsed event storage/fingerprints.
|
||||
- Verification passed: `(cd plugins/examples/scum-server-plugin/companion && go test ./...)` and `(cd plugins && npm run validate:manifest)`.
|
||||
|
||||
### Map asset implementation evidence (2026-08-13)
|
||||
|
||||
- Added plugin-owned SCUM current-service coordinate-map metadata and transform assets under `assets/scum-live/map/`, digest-referenced from `scumLiveData.mapAssets` as `sha256:74800836553c7e4372a0747c5e9511adcee940b057194488bbf65bf164df372b` and `sha256:f1941109167a86818e9e71996821884aeb8bd7e863584454b891e525695ba478`.
|
||||
- The packaged asset is explicitly limited to first-party-generated coordinate metadata and the observed current-service coordinate envelope from `evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md`; no unauthorized SCUM base-map artwork is shipped, and `positions.read` remains disabled until compatible evidence enables it.
|
||||
- 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)`.
|
||||
|
||||
### Current-service baseline refresh (2026-08-13)
|
||||
|
||||
- Per operator correction, the change now stores current-service structure baselines before attempting further capability declarations. `evidence/scum-current-service-db-schema-baseline-2026-08-13.md` records the full `161`-table compact schema inventory from a server-local read-only Python diagnostic, including row counts, columns, primary-key columns, foreign-key counts, and index counts without raw rows, SQL, XML, paths, credentials, sockets, IPs, or player identities.
|
||||
- `evidence/scum-current-service-log-structure-baseline-2026-08-13.md` records the process-adjacent log inventory from a successful server-local diagnostic: `1200` log/text files across hashed directories, including `login_{digits}.log`, `admin_{digits}.log`, `gameplay_{digits}.log`, `economy_{digits}.log`, `chat_{digits}.log`, `vehicle_destruction_{digits}.log`, `SCUM.log`, and service/runtime log families. Raw log lines and paths were not emitted or stored.
|
||||
- `evidence/scum-current-service-content-features-baseline-2026-08-13.md` records redacted content features from the current service: XML tag/attribute/value-shape summaries, selected DB content distributions, and log skeleton marker sets. It confirms `user_profile.template_xml` parses as `CharacterTemplate` with named character attributes and `Skill` entries, confirms sampled `prisoner_skill.xml` values are not parseable XML documents, and classifies `item_entity.xml` as item metadata rather than a profile attribute source.
|
||||
- A broader line-shape diagnostic confirmed coordinate-shaped and network-shaped tokens exist in multiple log families, so parser assets must stay per-file-pattern and strip network material before durable storage. The authoritative file inventory remains the successful v4 evidence file.
|
||||
- Task 3.10 remains unchecked: the refreshed content-feature baseline confirms `user_profile.template_xml` is the parseable named-attribute source candidate, but it still does not prove preserving patch semantics, offline/backup/readback requirements, activation semantics, write safety, or an operator-confirmed `855` preset mapping.
|
||||
|
||||
### Adapter evidence matrix and release gates (2026-08-13)
|
||||
|
||||
- Matched the production SCUM live-data adapter gates to the current-service schema fingerprint `sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0` and packaged query/map asset digests. The schema-probe gate is now `enabled` / `compatible` because the product durable Run schema-probe path succeeded, while the database-backed read gates remain `disabled` / `missing` until the group 4 SQLite-template terminal envelope is implemented and accepted.
|
||||
- Added manifest-validator enforcement that every declared SCUM SQLite query, typed command, guarded mutation, map asset, and gift catalog must have a matching capability gate with the same adapter version, schema fingerprint where applicable, and immutable asset digest. This prevents future assets from bypassing the release gate matrix.
|
||||
- Kept unsupported or ambiguous write capabilities disabled: no typed RCON templates, no gift catalogs, no guarded XML mutations, no `855` preset, and no command/readback claims were added. Squad rank/leader, vehicle ownership, territory semantics, and sub-10s map cadence remain nullable/gated instead of invented.
|
||||
- Verification passed: `(cd plugins && npm test -- manifest-validation.test.ts)`, `(cd plugins && npm run validate:manifest)`, and `(cd platform && go test ./validator ./domain)`.
|
||||
|
||||
- [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.
|
||||
- [x] 3.4 Implement the versioned SCUM login/logout parser and tests for successful login/logout, failed login, partial/undecodable/oversized/malformed lines, copy-truncate/rotation overlap under a new generation, Run restart/resume, duplicate delivery, and out-of-order delivery while discarding IP/network material before storage or logical fingerprinting.
|
||||
- [x] 3.5 Add parameterized, read-only player identity/detail/economy/session-enrichment query assets and exact result schemas only for joins and fields proven by the probe.
|
||||
- [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.
|
||||
- [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.
|
||||
- [x] 3.13 Match the observed fingerprint/evidence matrix against each completed adapter, add per-capability compatibility/release-gate tests, and leave every unsupported or ambiguous player/squad/vehicle/flag/position/write capability disabled.
|
||||
|
||||
## 4. Generic External Run Execution and Result Contracts
|
||||
|
||||
### Platform probe wiring evidence (2026-08-12)
|
||||
|
||||
- Platform now has an internal-only `RequestSCUMSchemaProbeForSession` path that builds a durable `remote.run.db.sqlite.probe` job from the SCUM plugin's manifest declaration, active runtime binding, logical target key, adapter version, and bounded probe limits; public remote-adapter and plugin-page requests for the probe capability are denied.
|
||||
- Run terminal results may carry `executionResult.sqliteSchemaProbe`; Platform DTO/domain/validator/job-channel code validates the typed redacted envelope, job/request identity, and binding fence before persisting it on the durable job.
|
||||
- Generated Run packages now carry redacted autonomous lifecycle `dataTargets` entries for plugin-owned sqlite snapshots, and browser-facing runtime-profile responses continue to omit those source declarations.
|
||||
- Focused evidence: `go test ./dto ./service -run 'Test(RunJobResultRequestParsesSQLiteSchemaProbeEnvelope|SCUMSchemaProbeDispatchIsPlatformScheduledAndFenced|RemoteAdapterRequestPropagatesTypedInputsToRunJob)'` and `(cd platform && go test ./...)` passed locally. These tests did not prove the active Windows Run deployment or current SCUM schema, so the later current-service and external-Run acceptance tasks stayed gated until additional evidence was recorded.
|
||||
|
||||
### SQLite-template contract freeze and Run handoff evidence (2026-08-13)
|
||||
|
||||
- Platform protocol docs now freeze the read-only `sqliteTemplate` request and `sqlite.template-query` terminal envelope: template key, logical target key, adapter/schema fingerprint, asset digest, parameter digest, bounded scalar parameters, query-only execution limits, row/result-byte limits, cancellation, and stable safe status/error codes.
|
||||
- Added Platform domain/DTO/job-channel/validator/service contracts for `SCUMSQLiteTemplateRequest` and `SCUMSQLiteTemplateResult`. Run assignments can carry only the typed template request, Run results can return only the typed envelope, and Platform verifies leased job identity, binding, capability, target/template key, schema fingerprint, asset digest, parameter digest, row count, and result digest before accepting a successful result.
|
||||
- Added focused tests for DTO parsing, safe validator rejection of raw SQL/path-like material and loose bounds, typed row/result validation, service lease fencing, and digest mismatch rejection. Verification passed: `go test ./dto ./validator ./service -run 'Test(RunJobResultRequestParsesSQLite|ValidateSCUMSQLiteTemplate|CoreServiceRunJobSQLiteTemplateEnvelopeIsFencedToLease)'`.
|
||||
- Recorded the separately rooted Run handoff prompt in `evidence/run-sqlite-template-execution-handoff-2026-08-13.md` with positive, directional, and boundary prompts. This is a contract handoff only; tasks 4.7, 4.9, and DB-backed read gates remain unchecked until tested Run commit/deployment/terminal-envelope evidence is recorded.
|
||||
|
||||
### Typed RCON-template contract freeze and Run handoff evidence (2026-08-13)
|
||||
|
||||
- Platform protocol docs now freeze the generic `rconTemplate` request and `rcon.template-command` terminal envelope: logical transport/target/template keys, adapter/schema fingerprint when required, asset digest, payload digest, confirmation digest, target identity digest, idempotency key, bounded scalar payload, safe review reason, response/confirmation limits, conclusive confirmation status, and stable safe result/error codes.
|
||||
- Added Platform domain/DTO/job-channel/validator/service contracts for `SCUMTypedRCONTemplateRequest` and `SCUMTypedRCONTemplateResult`. Run assignments can carry only the typed template request, not browser command text; Run results can return only safe digests/status/summary; Platform verifies leased job identity, binding, transport, template, schema, asset, payload, confirmation, and target digests before accepting success.
|
||||
- Added focused tests for DTO parsing, validator rejection of raw command-like payload keys, unsafe review reasons, loose bounds, unconfirmed success, unsafe summaries, service lease fencing, and payload digest mismatch rejection. Verification passed: `go test ./dto ./validator ./service -run 'Test(RunJobResultRequestParses(SQLite|TypedRCON)|ValidateSCUM(SQLiteTemplate|TypedRCON)|CoreServiceRunJob(SQLiteTemplate|TypedRCONTemplate)EnvelopeIsFencedToLease)'`.
|
||||
- Recorded the separately rooted Run handoff prompt in `evidence/run-typed-rcon-template-execution-handoff-2026-08-13.md` with positive, directional, and boundary prompts. This is a contract handoff only; typed RCON templates, gift catalogs, write gates, and task 4.7 remain unchecked until tested Run commit/deployment/current-service command/readback evidence is recorded.
|
||||
|
||||
### Guarded SQLite/XML mutation contract freeze and Run handoff evidence (2026-08-13)
|
||||
|
||||
- Platform protocol docs now freeze the generic `guardedMutation` request and `sqlite.guarded-mutation` terminal envelope: logical target/template key, adapter/schema fingerprint, asset digest, target identity digest, expected row/value/XML digests, preserving patch digest, backup/offline/danger-confirmation evidence digests, readback expectation digest, idempotency key, bounded scalar payload, safe review reason, single-row limit, affected-row count, readback status, and stable safe result/error codes.
|
||||
- Added Platform domain/DTO/job-channel/validator/service contracts for `SCUMGuardedMutationRequest` and `SCUMGuardedMutationResult`. Run assignments can carry only the typed guarded mutation request, not raw SQL/XML/browser mutation text; Run results can return only safe digests/status/summary; Platform verifies leased job identity, binding, template, schema, asset, target, guard, patch, backup, offline, danger confirmation, and readback digests before accepting success.
|
||||
- Added focused tests for DTO parsing, validator rejection of raw XML/SQL/path-like material, `855` field payloads, missing backup/offline/danger-confirmation/readback digests, loose affected-row bounds, unsafe summaries, multi-row success, missing readback, service lease fencing, and patch digest mismatch rejection. Verification passed: `go test ./dto ./validator ./service -run 'Test(RunJobResultRequestParses(SQLite|TypedRCON|GuardedMutation)|ValidateSCUM(SQLiteTemplate|TypedRCON|GuardedMutation)|CoreServiceRunJob(SQLiteTemplate|TypedRCONTemplate|GuardedMutation)EnvelopeIsFencedToLease)'`.
|
||||
- Recorded the separately rooted Run handoff prompt in `evidence/run-guarded-sqlite-xml-mutation-handoff-2026-08-13.md` with positive, directional, and boundary prompts. This is a contract handoff only; no guarded XML mutation asset, `855` preset, write gate, external Run implementation evidence, or real-service mutation acceptance is enabled by this task.
|
||||
- Session verification also passed: `(cd platform && go test ./...)`, `scripts/check-structure.sh`, `openspec validate replace-scum-projections-with-real-data-management --strict`, and `git diff --check`.
|
||||
|
||||
### Log-source tailing contract freeze and Run handoff evidence (2026-08-13)
|
||||
|
||||
- Platform protocol docs now freeze the generic `log.parsed-events` terminal envelope for plugin-declared log-source tailing/backfill: leased source/stream key, parser key/version, adapter version, parser asset digest, parser digest, first/last redacted source identity and stream-generation cursors, event count, tail state, replay/partial flags, logical event digests, payload digests, safe summaries, safe errors, and applied limits.
|
||||
- Added Platform domain/DTO/job-channel/validator/service contracts for `SCUMParsedLogBatchResult`, parsed events/cursors, batch bounds, and log-tail states. Run results can carry only sanitized scalar event payloads, not raw log lines, paths, globs, IP/network identifiers, SQL, XML, sockets, credentials, or unredacted player identities.
|
||||
- Service job completion accepts `log.parsed-events` only for `logs.backfill` jobs carrying a leased declared `file.tail` source, checks server/Run endpoint, source/stream key, plugin id/version when frozen, parser key/version/digest/adapter version when frozen, and single source identity/generation boundaries.
|
||||
- Added focused tests for DTO parsing, validator rejection of raw-line/network/path material, loose max-line bounds, missing parser digest, service lease fencing, and parser digest mismatch rejection. Verification passed: `go test ./dto ./validator ./service -run 'Test(RunJobResultRequestParses(SQLite|TypedRCON|GuardedMutation|ParsedLog)|ValidateSCUM(SQLiteTemplate|TypedRCON|GuardedMutation|ParsedLog)|CoreServiceRunJob(SQLiteTemplate|TypedRCONTemplate|GuardedMutation|ParsedLogBatch)EnvelopeIsFencedToLease)'`.
|
||||
- Recorded the separately rooted Run handoff prompt in `evidence/run-log-source-tailing-handoff-2026-08-13.md` with positive, directional, and boundary prompts. This is a contract handoff only; no external Run implementation evidence, parsed-login ingestion enablement, or player/session creation acceptance is enabled by this task.
|
||||
|
||||
### Run acceptance audit and Platform capability negotiation (2026-08-13)
|
||||
|
||||
- Re-audited the browser evidence files, ignored independent Run checkout, and `枣庄服务器` through server-management MCP. The audit found only the prior schema-probe/data-target Run commits and redacted current-service probe evidence; it found no independent Run implementation/deployment/acceptance evidence for generic SQLite-template execution, typed RCON-template execution, guarded SQLite/XML mutation execution, parsed log-source tailing, or their late/duplicate terminal-result behavior. The audit is recorded in `evidence/scum-capability-negotiation-and-run-acceptance-audit-2026-08-13.md`, and task 4.7 remains unchecked.
|
||||
- Added Platform-side read-only SCUM capability negotiation at `GET /api/v1/server-instances/{id}/scum/capabilities`. It evaluates each manifest gate independently for the active server/plugin/Run endpoint/runtime binding using the bound Run capability list and latest accepted typed terminal evidence for matching binding, adapter, schema fingerprint, database identity, and asset digests.
|
||||
- The negotiation route returns only safe capability state and reasons, dispatches no Run job, and does not expose terminal rows, SQL, XML, RCON text, host paths, DSNs, sockets, credentials, protected payloads, or raw current-service content.
|
||||
- Verification passed: `go test ./domain ./dto ./service ./api -run 'TestSCUMCapabilityNegotiation|TestSCUMSchemaProbeEndpointQueuesPlatformScheduledDurableJob|TestLegacySCUMEndpointsReturnNotFoundWithoutDispatchingJobs'`.
|
||||
|
||||
- [x] 4.1 Add Platform protocol contracts under `platform/protocol`, API DTOs under `platform/dto`, validation under `platform/validator`, and plugin contracts/assets under `plugins/sdk` and `plugins/schemas`, plus contract documentation/mocks for probes, read-only template execution, typed RCON, guarded SQLite/XML mutation, parsed log events, and terminal result envelopes.
|
||||
- [x] 4.2 Freeze the generic executor/result contract and hand off a separately authorized Run-repository task for packaged SQLite-template execution with query-only connections, bound parameters, one-statement validation, short busy/operation timeouts, cancellation, and row/result-byte limits.
|
||||
- [x] 4.3 Require the independent Run task to return typed envelopes containing server/plugin binding, adapter/schema version, template key, asset digest, job identity, observed time, checksum, rows or affected-row count, and stable safe result/error codes.
|
||||
- [x] 4.4 Require the independent Run task to implement generic plugin-owned typed RCON-template execution without accepting browser command text or adding branches for SCUM, SCUM keys, SCUM commands, or SCUM tables.
|
||||
- [x] 4.5 Require the independent Run task to implement generic guarded single-row SQLite/XML mutation execution with expected identity/value/checksum guards, a bounded transaction, preserving XML patching, rollback on zero/multiple affected rows, and read-after-write confirmation.
|
||||
- [x] 4.6 Require the independent Run task to implement or extend generic plugin-declared log-source tailing so cursor persistence, rotation, truncate, restart, partial-line buffering, parser digest fencing, logical event fingerprinting, and replay remain independent of SCUM-specific source paths.
|
||||
- [ ] 4.7 Verify from the independent Run task's acceptance evidence that control/job/log/artifact priorities, leases, fencing, acknowledgements, idempotency, and late/duplicate terminal-result handling remain intact for the new generic capabilities.
|
||||
- [x] 4.8 Add Platform-side capability negotiation so probe, player/squad/vehicle/flag/position reads, typed commands, gifts, and guarded mutations are gated independently for each active Run/plugin/adapter binding.
|
||||
- [ ] 4.9 After the external Run contract tests/commit/deployment evidence is available, wire immutable assets and digests into generated Run packages and add distribution/contract tests proving Platform sends only template keys, bounded parameters, adapter version, and expected digest.
|
||||
|
||||
## 5. Dedicated Platform SCUM Persistence
|
||||
|
||||
- [ ] 5.1 Define SCUM domain types and narrow repository/service interfaces in the required `platform/` directories, keeping API DTOs, database models, validation, and repository contracts separate.
|
||||
- [ ] 5.2 Add dedicated player, session, player-detail, source-event, sync-cursor, capability-evidence, and completed-generation records with server-scoped identities and nullable unknown fields.
|
||||
- [ ] 5.3 Add dedicated squad, squad-member, vehicle, flag/territory, and current-position records with source checksum/time, adapter version, generation, and server-scoped indexes.
|
||||
- [ ] 5.4 Add dedicated gift-package, gift-item, frozen-delivery, per-item receipt, eligibility-period reservation, notification-result, and immutable completed-delivery records.
|
||||
- [ ] 5.5 Implement normalized MySQL models and explicit migrations with uniqueness constraints for player/event/session identity, generation rows, gift idempotency, and concurrent eligibility reservations.
|
||||
- [ ] 5.6 Implement the default file-backend SCUM store as a platform-owned SQLite sidecar under the configured data directory, with pinned driver/migration behavior and no high-frequency writes to the global metadata snapshot.
|
||||
- [ ] 5.7 Implement the memory backend with the same server isolation, uniqueness, transaction, null, generation, and idempotency semantics for tests.
|
||||
- [ ] 5.8 Implement transactional generation commits so a complete validated scan may mark missing rows absent, while partial/failed/older scans preserve the last completed generation unchanged.
|
||||
- [ ] 5.9 Add health, migration, rollback/disable, and database-instance invalidation behavior; never migrate old projection/Workflow snapshot values into the new tables as game facts.
|
||||
- [ ] 5.10 Add backend-parity tests for migrations, uniqueness, concurrent first login, nullable numeric values, failed-generation retention, server isolation, and gift reservation/idempotency constraints.
|
||||
|
||||
## 6. Authentic Login Ingestion and Automatic Synchronization
|
||||
|
||||
- [ ] 6.1 Add a typed parsed-log event ingress that authenticates and correlates server binding, plugin/adapter/parser digest, transport cursor `(source identity, stream generation, sequence)`, separate stable logical event identity, and occurrence time before calling SCUM ingestion.
|
||||
- [ ] 6.2 Atomically upsert one `(server_instance_id, external_player_id)` player and open one session for an authentic successful login without waiting for database enrichment.
|
||||
- [ ] 6.3 Close only the matching current session on logout, do not fabricate a session for an unmatched logout, and prevent older logout/login events from regressing a newer display name, last-seen time, or online session.
|
||||
- [ ] 6.4 Close or mark sessions unknown with a bounded reason when a binding/source epoch is replaced, server stops, or log continuity is lost without a logout; never leave them permanently confirmed online from database save timestamps.
|
||||
- [ ] 6.5 Strip raw IP addresses and all network identifiers before durable player/session/event storage and exclude them from every SCUM API, diagnostic, and AI context.
|
||||
- [ ] 6.6 Add ingestion tests for concurrent first login, replayed events, duplicate/out-of-order events, failed login, unmatched logout, partial-line resume, copy-truncate/rotation overlap replay under a new generation, Run restart, parser-digest mismatch, cross-server events, and database timestamps that must not imply online state; an overlapped logical login SHALL still produce one player/session.
|
||||
- [ ] 6.7 Add an automatic scheduler that starts a capability-specific probe when an active binding lacks current evidence, starts an initial bounded scan after compatibility succeeds, then uses plugin-declared jittered cadences, backoff, and per-server/per-capability concurrency limits.
|
||||
- [ ] 6.8 Schedule bounded player-detail enrichment after a new login without delaying player creation, and use the measured safe position cadence or a declared verified companion source without fabricated intermediate motion.
|
||||
- [ ] 6.9 Create every durable Run job before dispatch and attach the expected server/plugin/template/digest/schema/generation correlation needed for immediate, late, and duplicate results.
|
||||
- [ ] 6.10 Add a terminal-job hook that validates the full result envelope and row schema before calling the matching transactional SCUM sync service.
|
||||
- [ ] 6.11 Reject malformed, foreign, duplicate, or older results idempotently; commit only complete generations and retain prior rows plus a safe connection/sync error after locks, timeouts, partial results, or incompatible schemas.
|
||||
- [ ] 6.12 Publish safe player/session/squad/map updates through a platform-owned event stream/SSE while keeping Platform Web reads backed by local records.
|
||||
- [ ] 6.13 Add tests proving page opens/retries never create schema probes, Run queries, projection refreshes, real-data refreshes, manual syncs, or audit jobs.
|
||||
|
||||
## 7. Player Management APIs
|
||||
|
||||
- [ ] 7.1 Add safe player-list/detail/session DTOs, request schemas, validators, API clients/contracts, and routes in their fixed directories without exposing raw logs, database rows, XML, SQL, paths, or network material.
|
||||
- [ ] 7.2 Implement server-side player pagination, bounded name/external-ID search, online and squad filters, deterministic allowlisted sorting, and total/page metadata against the local SCUM store.
|
||||
- [ ] 7.3 Implement player detail with identity, verified nullable facts, current verified coordinate, bounded login history, source collection times, and explicit not-yet-synchronized/confirmed-empty/incompatible/connection-failed states.
|
||||
- [ ] 7.4 Enforce target-server read authorization before lookup and prevent cross-server player IDs, squad filters, selectors, counts, or existence from leaking.
|
||||
- [ ] 7.5 Preserve unknown values as null/absent throughout storage, service, DTO, and JSON handling; never substitute zero, sample data, guessed profile IDs, or database save time as online evidence.
|
||||
- [ ] 7.6 Remove player-intelligence, alias-history, shared-IP, access-attempt, automatic-risk, security-signal, and hard-coded increment dependencies from the SCUM player API and ingestion flow.
|
||||
- [ ] 7.7 Add repository/service/API tests for search/filter/sort bounds, pagination stability, confirmed-empty versus unavailable data, nullable facts, login history, authorization, cross-server isolation, and absence of manual-refresh endpoints.
|
||||
|
||||
## 8. Squad Management and Realtime Map APIs
|
||||
|
||||
- [ ] 8.1 Implement server-scoped paginated/searchable/sortable squad list and detail APIs with verified members, adapter-declared rank meanings, leader when proven, flags/territory, and ordinary collection times.
|
||||
- [ ] 8.2 Keep leader, rank, territory, and ownership unknown when joins or enum meanings are ambiguous, gate squad/member/flag/territory resources independently, and test that reference constants or proximity/history heuristics are not evidence.
|
||||
- [ ] 8.3 Implement bounded local vehicle and flag APIs containing only verified identity, class/status, coordinate, ownership, and collection fields supported by each active adapter capability.
|
||||
- [ ] 8.4 Implement a safe current-map dataset API for players, vehicles, flags, squads/territories, layer filters, and source collection times without returning database or Run connection material.
|
||||
- [ ] 8.5 Apply the plugin-declared map version/bounds/coordinate transform server-side or through a shared tested contract, rejecting incompatible, non-finite, and out-of-bounds coordinates instead of generating fallback points.
|
||||
- [ ] 8.6 Publish newer verified position/map updates through the platform event stream with entity identity and server/version fencing; page subscriptions must not dispatch Run reads.
|
||||
- [ ] 8.7 Enforce server authorization, bounded selectors/result sizes, and no cross-server existence leaks across all squad/map endpoints.
|
||||
- [ ] 8.8 Add service/API tests for successful and failed generations, per-resource gating, unknown ownership/ranks, last-complete rows after interruption, transform fixtures, layer filtering, event ordering, and incompatible-map unavailable results.
|
||||
|
||||
## 9. Gift Management, Eligibility, and Delivery
|
||||
|
||||
- [ ] 9.1 Add server/plugin-version-scoped gift package and typed item validators for names, classification, active state, quantities, eligibility rules, period limits, and only plugin-catalogued item keys.
|
||||
- [ ] 9.2 Implement `server.game-client.read` package/history reads, `server.game-client.maintenance` package create/update/enable/delete, and `server.game-client.command` reviewed delivery APIs using the current session's effective target-server permissions.
|
||||
- [ ] 9.3 Evaluate per-player/server/period eligibility in the server's declared timezone and reserve limit capacity transactionally for in-flight, partial, and unknown deliveries so concurrent requests cannot exceed the configured limit.
|
||||
- [ ] 9.4 Freeze target player, package/items, quantities, plugin/game/adapter version, period reservation, delivery identity, and idempotency key before dispatch; later package edits must not alter a delivery.
|
||||
- [ ] 9.5 Dispatch only plugin-declared typed item aliases and quantities through the controlled command path, never arbitrary browser command strings.
|
||||
- [ ] 9.6 Treat queued, claimed, acknowledged, or started jobs as in progress; record `delivered` only after a schema-valid conclusive receipt for every required item.
|
||||
- [ ] 9.7 Preserve reservations and per-item receipts for timed-out, missing, partial, or unknown outcomes, require confirmation before an explicit retry, and never automatically redeliver the whole package or already confirmed items.
|
||||
- [ ] 9.8 Release a period reservation only after conclusive evidence that no game effect occurred; record post-delivery notification failure separately without changing the delivered fact or triggering redelivery.
|
||||
- [ ] 9.9 Add server-scoped package statistics, searchable/filterable pagination, real player selection, reviewed send requests, and ordinary delivery-history/result APIs without Workflow or audit terminology.
|
||||
- [ ] 9.10 Add concurrency, idempotency, timezone-boundary, cross-server, catalog-version, partial/unknown outcome, reservation-release, notification-failure, permission, and immutable-history tests.
|
||||
|
||||
## 10. Controlled Manual and AI/Agent Writes
|
||||
|
||||
- [ ] 10.1 Define one named-field write draft containing server/player/action/field, verified current value/checksum, proposed value, reason, adapter/digest, idempotency key, safety requirements, and a safe reviewable diff.
|
||||
- [ ] 10.2 Authorize the current user's effective target-server permission when a draft is created, reviewed, confirmed, and dispatched, with backend checks authoritative and no component-principal, manifest-declaration, or callback-presence bypass.
|
||||
- [ ] 10.3 Supply the plugin page host with the current session's effective permissions and readable denial reasons while keeping direct API denial authoritative.
|
||||
- [ ] 10.4 Route verified Fame/cash/gold writes through plugin-owned typed RCON templates with `server.game-client.command`, validated absolute target values, explicit reason, idempotency, and declared confirmation reads.
|
||||
- [ ] 10.5 Route database/XML writes only with effective `server.game-client.maintenance`, verified target/offline or maintenance state when required, genuine same-instance restorable backup evidence, expected before values/checksum, and an explicit dangerous-operation confirmation; do not create a platform-admin approval workflow or approval queue.
|
||||
- [ ] 10.6 Execute preserving named-attribute XML patches only against the probe-confirmed source, reject malformed XML or absent/undeclared nodes, preserve unknown content, and update exactly one guarded row.
|
||||
- [ ] 10.7 Keep `855` absent until its named mapping is confirmed; when available, expand it into an explicit per-attribute before/after review rather than accepting `fieldKey=855`, `prisoner.value`, or a generic integer.
|
||||
- [ ] 10.8 Validate command/mutation terminal envelopes and readback before success, update local verified details only after conclusive confirmation, and represent missing/mismatched results as failed, conflict, or unknown without automatic retry.
|
||||
- [ ] 10.9 Never chain kill, death, respawn, kick, or another destructive activation to attribute save; any verified required activation must be a separate explicitly named, permission-checked, confirmed action.
|
||||
- [ ] 10.10 Preserve AI-assisted plugin configuration through the existing platform-mediated reviewable config-diff path without exposing provider keys or granting the plugin page direct write authority.
|
||||
- [ ] 10.11 Make AI/Agent player-operation suggestions create the exact same named-field draft as manual forms, reject undeclared fields/protected payloads, retain the initiating user, and require that user's current effective permission plus explicit confirmation.
|
||||
- [ ] 10.12 Add tests for read-only users, revoked permissions between draft and dispatch, cross-server targets, invented AI fields, stale checksums, fake backup evidence, unsafe online state, zero/multiple rows, malformed XML, missing nodes, unknown results, confirmation mismatch, duplicate requests, and no approval-queue creation.
|
||||
|
||||
## 11. Five-Tab SCUM Product Surface
|
||||
|
||||
- [ ] 11.1 Place SCUM API clients/types, route definitions, page contracts, component contracts, schemas/validators, bridge/SDK types, and shared utilities in their fixed frontend/plugin directories rather than inside page components.
|
||||
- [x] 11.2 Make the SCUM detail navigation contain exactly `用户管理`, `队伍管理`, `实时地图`, `礼包管理`, `AI 助手` in that order, default to `用户管理`, and fall back from legacy `manage`, `workflows`, or invalid sections without affecting non-SCUM plugins.
|
||||
- [ ] 11.3 Preserve the existing server-list deployment action and relocate display-name and administrator-membership controls to a compact detail-header settings drawer/dialog with existing owner authorization; do not add another permanent management tab.
|
||||
- [ ] 11.4 Build `用户管理` as a full-width server-paginated table with bounded filters/search/sort, online evidence, nullable verified facts, detail/login-history drawer, and explicit named-field edit dialogs.
|
||||
- [ ] 11.5 Build `队伍管理` as a full-width paginated squad table and semantic detail drawer separating leader/ranks, members, flags, and territory while showing unknown facts honestly.
|
||||
- [ ] 11.6 Build `实时地图` from the authorized map asset and tested transform with distinct player/vehicle/flag/territory layers, filters, legend, source coordinates/collection time, safe live updates, and a clear incompatible/unavailable state.
|
||||
- [ ] 11.7 Build `礼包管理` with real package statistics/table, CRUD dialogs, typed items and limits, real player selection, reviewed delivery, and delivery history for in-progress/delivered/failed/partial/unknown/notification-failure outcomes.
|
||||
- [ ] 11.8 Keep `AI 助手` as the final tab for plugin configuration diffs and controlled player-operation drafts, with apply disabled when effective permission is absent.
|
||||
- [ ] 11.9 Hide or disable write controls according to current effective permissions with textual reasons, and re-check authorization server-side on every apply request.
|
||||
- [ ] 11.10 Load only platform-local resource APIs and the platform event stream; display ordinary connection and last synchronized/collected information, and let retry repeat only a local read.
|
||||
- [ ] 11.11 Reuse shared tables, drawers, dialogs, status, `console-*`, and theme tokens; preserve black-mecha and magical-girl readability, keep CSS declarations compressed, and add no page-local fixed decoration or generic opaque SaaS card system.
|
||||
- [ ] 11.12 Cover keyboard/focus behavior, non-color-only status, responsive full-width working surfaces, bounded compact actions, and readable destructive confirmations.
|
||||
- [ ] 11.13 Add frontend tests for exact navigation/order/default/fallback, settings ownership, local-only loading, permission presentation, null/empty/error states, real map/gift data, AI review parity, and absence of fake actions or placeholder records.
|
||||
|
||||
## 12. Projection, Workflow, Intelligence, and Placeholder Removal
|
||||
|
||||
- [x] 12.1 Inventory references before deletion and distinguish SCUM-only projection/Workflow/player-intelligence code from generic durable Run jobs, internal write evidence, and non-SCUM consumers.
|
||||
- [x] 12.2 Remove SCUM Workflow instance/step/status APIs, repositories, services, routes, clients, manifest declarations, page components, workflow creation/listing, pending-review counters, operation approval routes, and approval/confirmation queue surfaces without removing generic Run job execution.
|
||||
- [ ] 12.3 Remove SCUM projection/observation/freshness snapshot types, ingestion, metadata fields, refresh/audit services, page actions, and manual synchronization endpoints; removed endpoints must return not found or a stable removal response and dispatch no job.
|
||||
- [ ] 12.4 Remove SCUM dependencies on alias history, shared IP/fingerprint, access attempts, automatic risk/security signals, and player intelligence; delete shared implementation only after proving it has no remaining non-SCUM consumer.
|
||||
- [x] 12.5 Remove the standalone `管理` and `Workflow 状态` tabs, legacy placeholders/routes, fake maintenance/backup evidence, hard-coded increments, opaque `855` action, hard-coded `starter-pack`, fixed notification, gradient-only map, arbitrary percentage points, and sample/generated players/world data.
|
||||
- [x] 12.6 Remove runtime product copy including `Workflow 状态`, `投影`, `真实投影`, `玩家投影`, `刷新投影`, `刷新世界投影`, `刷新真实数据`, `发起审计`, `创建发放 workflow`, `typed workflow`, `typed observation`, `typed operation`, `待审操作`, `审批/确认队列`, `清理旧入口`, `目前暂无真实投影数据`, `暂无真实投影数据`, `暂无玩家投影`, and `Companion 可用`.
|
||||
- [ ] 12.7 Add upgrade behavior that starts the new SCUM stores empty, populates only from post-upgrade authenticated logs/current-service sync, invalidates incompatible bindings, and never translates old snapshot values into real facts.
|
||||
- [ ] 12.8 Add a rollback/feature-disable path that disables incompatible SCUM reads/writes while leaving diagnostic local records intact and never re-enables fake projection or Workflow data.
|
||||
- [x] 12.9 Add scoped runtime-source/manifest/API tests or assertions proving banned copy/actions/routes are absent, removed endpoints cannot dispatch jobs, and generic lifecycle, logs, jobs, AI provider management, and non-SCUM plugin navigation still work.
|
||||
- [x] 12.10 Record the supersession mapping from the completed-but-unarchived legacy SCUM changes to these unique replacement capabilities; do not archive obsolete deltas into the main baseline, and leave any history consolidation to a separate reviewed skip-specs/equivalent archival task.
|
||||
|
||||
## 13. End-to-End Verification and Release
|
||||
|
||||
- [ ] 13.1 Run focused Go tests after each Platform repository, migration, ingestion, scheduler, API, gift, permission, and terminal-result change, then run `(cd platform && go test ./...)`.
|
||||
- [ ] 13.2 Run plugin SDK/parser/query/map/manifest tests and final checks with `(cd plugins && npm run typecheck && npm run test && npm run validate:manifest)`, then run `(cd plugins/examples/scum-server-plugin/companion && go test ./...)`.
|
||||
- [ ] 13.3 Run frontend tests and final checks with `(cd platform_web && npm run typecheck && npm run test && npm run build)`.
|
||||
- [x] 13.4 In the separately authorized Run-repository task, run `go test ./...` from that repository's own root and record its tested commit/version plus deployment compatibility evidence here; do not edit, stage, or commit Run source from the browser-repository apply task.
|
||||
- [ ] 13.5 Against the active current service, verify read-only schema compatibility, authentic login-created local player/session data, automatic player/squad/vehicle/flag/position sync, generation retention after an induced safe read failure, and no unbound copied/cache/fixture database use.
|
||||
- [ ] 13.6 Verify login-log acceptance with sanitized real fixtures covering partial lines, failed login, rotation, truncate, restart/resume, duplicate, and out-of-order events plus server/Run binding/plugin/parser-digest fencing.
|
||||
- [ ] 13.7 Extend and run `scripts/browser-acceptance.sh` against synchronized local data for the exact five tabs, local-only page reads, user/squad/map/gift behavior, permission-aware edit reviews, AI configuration/player drafts, legacy-route fallback, and absence of projection/Workflow/manual-refresh/audit controls.
|
||||
- [ ] 13.8 Verify controlled writes against isolated test data or an explicitly authorized test player only; prove permission, explicit confirmation, guards, backup/offline requirements, idempotency, readback, unknown-result handling, XML preservation, and no implicit respawn.
|
||||
- [ ] 13.9 Perform scoped security checks proving browser/API/AI/job-safe responses contain no raw SQL, RCON, XML, host/database paths, credentials, sockets, IP data, or cross-server resource existence, and that external Run has no SCUM-specific executor branches.
|
||||
- [ ] 13.10 Run `scripts/check-structure.sh` and fix every relevant structural violation without moving implementation outside its owning root.
|
||||
- [ ] 13.11 Run `openspec validate replace-scum-projections-with-real-data-management --strict`, review task evidence and the final diff, and leave any task unchecked if its real-service, external-Run, test, or safety evidence is missing.
|
||||
- [ ] 13.12 On `main`, stage only files belonging to this change, create a concise commit after all required verification succeeds, and push the configured remote without including unrelated pre-existing worktree changes.
|
||||
@@ -103,8 +103,6 @@ func (h *coreHandlers) register(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/game-gifts/{catalogId}/revisions", h.serverGameGiftCatalogRevisions)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/game-gift-grants", h.serverGameGiftGrants)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/game-gift-grants/{grantId}/approve", h.serverGameGiftGrantApprove)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/scum/capabilities", h.serverSCUMCapabilities)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/scum/schema-probe", h.serverSCUMSchemaProbe)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/scum/players", h.serverSCUMPlayers)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/scum/squads", h.serverSCUMSquads)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/scum/squad-members", h.serverSCUMSquadMembers)
|
||||
|
||||
@@ -17,7 +17,7 @@ All routes use JSON request and response bodies. Collection routes support `GET`
|
||||
| Server runtime distribution | n/a | `GET /api/v1/server-instances/{id}/runtime/actions`, `POST /api/v1/server-instances/{id}/run/generate`, `POST /api/v1/server-instances/{id}/run/download`, `POST /api/v1/server-instances/{id}/run/key/reset`, `POST /api/v1/server-instances/{id}/run/update`, `GET /api/v1/server-instances/{id}/run/update`, `POST /api/v1/server-instances/{id}/client-managers/generate`, `POST /api/v1/server-instances/{id}/client-managers/download`, `POST /api/v1/server-instances/{id}/client-managers/key/reset`, `GET /api/v1/server-instances/{id}/dependencies`, `POST /api/v1/server-instances/{id}/dependencies/check`, `POST /api/v1/server-instances/{id}/dependencies/install` | `ServerRuntimeActionsResponse`, `RunDistributionGenerateRequest`, `RunDistributionResponse`, `RunUpdateRequest`, `RunUpdateJobResponse`/`RunUpdateJobListResponse`, `ClientManagerBuildRequest`, `ClientManagerDistributionResponse`, `ClientManagerDownloadRequest`, `ComponentKeyResetRequest`, `ComponentKeyResponse`, `DependencyCatalogResponse`, `DependencyJobRequest` |
|
||||
| Metrics | `GET /api/v1/metrics/platform`, `GET /api/v1/metrics/server-instances` | n/a | `PlatformResourceUsageResponse`, `ServerMetricsResponse`, `ServerMetricsListResponse` |
|
||||
| File operations | `POST /api/v1/file-operations/dispatch` | n/a | `FileOperationDispatchRequest`, `FileOperationDispatchResponse` |
|
||||
| SCUM local resources | n/a | `GET /api/v1/server-instances/{id}/scum/players`, `GET .../scum/squads`, `GET .../scum/squad-members`, `GET .../scum/vehicles`, `GET .../scum/flags`, `GET .../scum/positions`; removed legacy SCUM execution routes return `404` and dispatch no job | `SCUM*Response`, `ErrorResponse` |
|
||||
| SCUM projections and workflows | n/a | `GET /api/v1/server-instances/{id}/scum/players`, `GET .../scum/squads`, `GET .../scum/squad-members`, `GET .../scum/vehicles`, `GET .../scum/flags`, `GET .../scum/positions`, `GET/POST .../scum/operations`, `POST .../scum/operations/{operationId}/approve`, `GET/POST .../scum/workflows`, `GET .../scum/workflow-steps` | `SCUM*Response`, `SCUMOperationRequestBody`, `SCUMWorkflowCreateRequest`, safe operation/workflow summaries |
|
||||
| Server administrators | `GET /api/v1/server-instances/{id}/administrators/candidates`, `POST /api/v1/server-instances/{id}/administrators` | `DELETE /api/v1/server-instances/{id}/administrators/{userId}` | `ServerMemberRequest`, `ServerMemberResponse`, `ServerMemberListResponse`, `ServerInstanceResponse` |
|
||||
| Run endpoints | `GET /api/v1/run/endpoints`, `POST /api/v1/run/endpoints` | `GET /api/v1/run/endpoints/{id}` | `RunEndpointCreateRequest`, `RunEndpointResponse`, `RunEndpointListResponse` |
|
||||
| Jobs | `GET /api/v1/jobs`, `POST /api/v1/jobs` | `GET /api/v1/jobs/{id}` | `JobCreateRequest`, `JobResponse`, `JobListResponse` |
|
||||
@@ -161,7 +161,7 @@ Server-scoped terminal log streaming (`GET /api/v1/server-instances/{id}/logs/ev
|
||||
|
||||
Runtime distribution and client-manager APIs require the current bearer session, server visibility, plugin-declared permissions, complete runtime bindings only for actions that truly depend on external logical bindings, and platform-builder readiness. Run-side lifecycle commands separately require run endpoint capability support and use plugin-declared lifecycle actions without making manual runtime-profile binding a user prerequisite. Responses and audit summaries expose artifact IDs, job IDs, checksums, key generations, fingerprints, status, and redacted `secret://runtime-keys/.../current` refs only. They do not expose raw run keys, client-manager keys, FTP passwords, database DSNs, RCON passwords, host paths, direct sockets, run endpoint private addresses, build workspace paths, or large inline logs.
|
||||
|
||||
SCUM product APIs expose only safe local resource rows, capability availability, collected timestamps, and redacted status reasons. `GET /api/v1/server-instances/{id}/scum/capabilities` returns the Platform-negotiated gate state for the active Run/plugin/adapter binding without dispatching a job. Removed legacy SCUM execution routes return `404` and dispatch no job. SCUM APIs never expose SCUM.db SQL text, DB paths, DSNs, RCON command text, raw protected request payloads, run sockets, host paths, or credentials.
|
||||
SCUM product APIs expose only safe local projections, typed operation/workflow requests, approval status, confirmation status, blocker reasons, and audit-safe summaries. They never expose SCUM.db SQL text, DB paths, DSNs, RCON command text, raw protected request payloads, run sockets, host paths, or credentials.
|
||||
|
||||
`POST /api/v1/server-instances/workflows/create` requires only the plugin type and server name. A runtime binding may still be maintained internally for advanced logical transports, but browser lifecycle controls must not force operators to choose a runtime profile before start/stop or run-package generation when the plugin deployment/lifecycle declaration is sufficient. Platform builds distributions itself and never needs a registered Run endpoint with `distribution.build` to do so.
|
||||
|
||||
|
||||
+122
-45
@@ -2,47 +2,23 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/dto"
|
||||
)
|
||||
|
||||
func (h *coreHandlers) serverSCUMCapabilities(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
negotiation, err := h.core.NegotiateSCUMCapabilitiesForSession(bearerToken(r), r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMCapabilityNegotiationFromDomain(negotiation))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMSchemaProbe(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
return
|
||||
}
|
||||
request, err := decodeJSON[dto.SCUMSchemaProbeDispatchRequest](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
probeRequest, queued, err := h.core.RequestSCUMSchemaProbeForSession(bearerToken(r), r.PathValue("id"), request.IdempotencyKey)
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusAccepted, dto.SCUMSchemaProbeDispatchFromDomain(probeRequest, queued))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMPlayers(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
writeRemovedSCUMEndpoint(w)
|
||||
items, err := h.core.ListSCUMPlayerLiveStatesForSession(bearerToken(r), scumProjectionFilterFromRequest(r, r.PathValue("id")))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMPlayerLiveStatesFromDomain(items))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMSquads(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -50,7 +26,12 @@ func (h *coreHandlers) serverSCUMSquads(w http.ResponseWriter, r *http.Request)
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
writeRemovedSCUMEndpoint(w)
|
||||
items, err := h.core.ListSCUMSquadsForSession(bearerToken(r), scumProjectionFilterFromRequest(r, r.PathValue("id")))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMSquadsFromDomain(items))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMSquadMembers(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -58,7 +39,12 @@ func (h *coreHandlers) serverSCUMSquadMembers(w http.ResponseWriter, r *http.Req
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
writeRemovedSCUMEndpoint(w)
|
||||
items, err := h.core.ListSCUMSquadMembersForSession(bearerToken(r), scumProjectionFilterFromRequest(r, r.PathValue("id")))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMSquadMembersFromDomain(items))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMVehicles(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -66,7 +52,12 @@ func (h *coreHandlers) serverSCUMVehicles(w http.ResponseWriter, r *http.Request
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
writeRemovedSCUMEndpoint(w)
|
||||
items, err := h.core.ListSCUMVehiclesForSession(bearerToken(r), scumProjectionFilterFromRequest(r, r.PathValue("id")))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMVehiclesFromDomain(items))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMFlags(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -74,7 +65,12 @@ func (h *coreHandlers) serverSCUMFlags(w http.ResponseWriter, r *http.Request) {
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
writeRemovedSCUMEndpoint(w)
|
||||
items, err := h.core.ListSCUMFlagsForSession(bearerToken(r), scumProjectionFilterFromRequest(r, r.PathValue("id")))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMFlagsFromDomain(items))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMPositions(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -82,13 +78,36 @@ func (h *coreHandlers) serverSCUMPositions(w http.ResponseWriter, r *http.Reques
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
writeRemovedSCUMEndpoint(w)
|
||||
items, err := h.core.ListSCUMCurrentPositionsForSession(bearerToken(r), scumProjectionFilterFromRequest(r, r.PathValue("id")))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMCurrentPositionsFromDomain(items))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMOperations(w http.ResponseWriter, r *http.Request) {
|
||||
serverID := r.PathValue("id")
|
||||
switch r.Method {
|
||||
case http.MethodGet, http.MethodPost:
|
||||
writeRemovedSCUMEndpoint(w)
|
||||
case http.MethodGet:
|
||||
items, err := h.core.ListSCUMOperationsForSession(bearerToken(r), scumOperationFilterFromRequest(r, serverID))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMOperationsFromDomain(items))
|
||||
case http.MethodPost:
|
||||
request, err := decodeJSON[dto.SCUMOperationRequestBody](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
operation, err := h.core.RequestSCUMOperationForSession(bearerToken(r), serverID, dto.SCUMOperationRequestBodyToDomain(request))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, dto.SCUMOperationFromDomain(operation))
|
||||
default:
|
||||
writeMethodNotAllowed(w, "GET, POST")
|
||||
}
|
||||
@@ -99,13 +118,36 @@ func (h *coreHandlers) serverSCUMOperationApprove(w http.ResponseWriter, r *http
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
return
|
||||
}
|
||||
writeRemovedSCUMEndpoint(w)
|
||||
operation, err := h.core.ApproveSCUMOperationForSession(bearerToken(r), r.PathValue("operationId"))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMOperationFromDomain(operation))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMWorkflows(w http.ResponseWriter, r *http.Request) {
|
||||
serverID := r.PathValue("id")
|
||||
switch r.Method {
|
||||
case http.MethodGet, http.MethodPost:
|
||||
writeRemovedSCUMEndpoint(w)
|
||||
case http.MethodGet:
|
||||
items, err := h.core.ListSCUMWorkflowsForSession(bearerToken(r), scumWorkflowFilterFromRequest(r, serverID))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMWorkflowsFromDomain(items))
|
||||
case http.MethodPost:
|
||||
request, err := decodeJSON[dto.SCUMWorkflowCreateRequest](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
workflow, err := h.core.CreateSCUMWorkflowForSession(bearerToken(r), serverID, dto.SCUMWorkflowCreateRequestToDomain(request))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, dto.SCUMWorkflowFromDomain(workflow))
|
||||
default:
|
||||
writeMethodNotAllowed(w, "GET, POST")
|
||||
}
|
||||
@@ -116,9 +158,44 @@ func (h *coreHandlers) serverSCUMWorkflowSteps(w http.ResponseWriter, r *http.Re
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
writeRemovedSCUMEndpoint(w)
|
||||
items, err := h.core.ListSCUMWorkflowStepsForSession(bearerToken(r), scumWorkflowStepFilterFromRequest(r, r.PathValue("id")))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMWorkflowStepsFromDomain(items))
|
||||
}
|
||||
|
||||
func writeRemovedSCUMEndpoint(w http.ResponseWriter) {
|
||||
writeAPIError(w, http.StatusNotFound, errorCodeNotFound, "legacy SCUM endpoint removed; use the local SCUM management APIs", nil)
|
||||
func scumProjectionFilterFromRequest(r *http.Request, serverID string) domain.SCUMProjectionFilter {
|
||||
query := r.URL.Query()
|
||||
return domain.SCUMProjectionFilter{ServerInstanceID: serverID, GamePlayerID: query.Get("gamePlayerId"), GamePlayerRecordID: query.Get("gamePlayerRecordId"), UserProfileID: query.Get("userProfileId"), SteamID: query.Get("steamId"), SquadID: query.Get("squadId"), VehicleID: query.Get("vehicleId"), FlagID: query.Get("flagId"), SubjectType: domain.SCUMProjectionSubject(query.Get("subjectType")), QueryKey: query.Get("queryKey"), Freshness: domain.SCUMProjectionFreshness(query.Get("freshness")), Search: query.Get("search"), Limit: boundedQueryLimit(query.Get("limit"), 200)}
|
||||
}
|
||||
|
||||
func scumOperationFilterFromRequest(r *http.Request, serverID string) domain.SCUMOperationRequestFilter {
|
||||
query := r.URL.Query()
|
||||
return domain.SCUMOperationRequestFilter{ServerInstanceID: serverID, TemplateKey: query.Get("templateKey"), PlayerID: query.Get("playerId"), RequesterID: query.Get("requesterId"), Status: domain.SCUMWorkflowStepStatus(query.Get("status")), IdempotencyKey: query.Get("idempotencyKey"), Limit: boundedQueryLimit(query.Get("limit"), 100)}
|
||||
}
|
||||
|
||||
func scumWorkflowFilterFromRequest(r *http.Request, serverID string) domain.SCUMWorkflowInstanceFilter {
|
||||
query := r.URL.Query()
|
||||
return domain.SCUMWorkflowInstanceFilter{ServerInstanceID: serverID, TemplateKey: query.Get("templateKey"), RequestedBy: query.Get("requestedBy"), Status: domain.SCUMWorkflowStatus(query.Get("status")), IdempotencyKey: query.Get("idempotencyKey"), Limit: boundedQueryLimit(query.Get("limit"), 100)}
|
||||
}
|
||||
|
||||
func scumWorkflowStepFilterFromRequest(r *http.Request, serverID string) domain.SCUMWorkflowStepFilter {
|
||||
query := r.URL.Query()
|
||||
return domain.SCUMWorkflowStepFilter{ServerInstanceID: serverID, WorkflowID: query.Get("workflowId"), StepKey: query.Get("stepKey"), Status: domain.SCUMWorkflowStepStatus(query.Get("status")), Limit: boundedQueryLimit(query.Get("limit"), 200)}
|
||||
}
|
||||
|
||||
func boundedQueryLimit(raw string, fallback int) int {
|
||||
if raw == "" {
|
||||
return fallback
|
||||
}
|
||||
parsed, err := strconv.Atoi(raw)
|
||||
if err != nil || parsed < 1 {
|
||||
return fallback
|
||||
}
|
||||
if parsed > 500 {
|
||||
return 500
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
@@ -1,124 +1,148 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/dto"
|
||||
"browser.local/platform/repo"
|
||||
"browser.local/platform/service"
|
||||
)
|
||||
|
||||
func TestSCUMSchemaProbeEndpointQueuesPlatformScheduledDurableJob(t *testing.T) {
|
||||
store := repo.NewMemoryStore()
|
||||
core := service.NewCoreService(store)
|
||||
plugin, err := core.CreateGamePlugin(domain.GamePlugin{
|
||||
ID: "server.scum",
|
||||
Name: "SCUM",
|
||||
Version: "1.0.0",
|
||||
ServerType: "scum",
|
||||
ManifestRef: "artifact://manifests/server.scum/1.0.0",
|
||||
CreateFormSchemaRef: "artifact://schemas/server.scum/create-form/1.0.0",
|
||||
RequiredRunCapabilities: []string{"process.start", domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunDBSQLiteProbe},
|
||||
DeclaredPermissions: []string{"server.remote.access"},
|
||||
Permissions: domain.PluginPermissions{Jobs: true, RemoteAccess: true},
|
||||
LifecycleActions: domain.PluginLifecycleActions{Start: "actions/start.json"},
|
||||
RemoteAccess: domain.GamePluginRemoteAccess{Methods: []string{"run"}, RunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}},
|
||||
RuntimeProfiles: domain.GamePluginRuntimeProfiles{
|
||||
LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{"process.start"}}},
|
||||
TransportProfiles: []domain.RuntimeTransportProfile{
|
||||
{Key: "server-files", Kind: "file", TargetKey: "server-root", Capabilities: []string{domain.JobCapabilityRemoteRunFilesRead}},
|
||||
{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}},
|
||||
},
|
||||
DataTargets: []domain.RuntimeDataTarget{{Key: "scum-database", Kind: "sqlite.snapshot", TransportKey: "scum-database", SourceRootKey: "server-root", SourcePath: "SCUM/Saved/SaveFiles/SCUM.db", WorkspaceKey: "databases/scum-database", RefreshPolicy: "on-demand-snapshot", MaxBytes: 1024 * 1024 * 1024, Platforms: []string{"windows"}}},
|
||||
},
|
||||
SCUMLiveData: domain.SCUMLiveDataManifest{SchemaVersion: "1", Probe: domain.SCUMSchemaProbeDeclaration{Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, TargetKey: "scum-database", Bounds: domain.DefaultSCUMSchemaProbeBounds()}, CapabilityGates: []domain.SCUMLiveDataCapabilityGateDeclaration{{Capability: domain.SCUMDataCapabilitySchemaProbe, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "scum-live-data-v0", EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "waiting for current service evidence"}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create plugin: %v", err)
|
||||
}
|
||||
endpoint, err := core.CreateRunEndpoint(domain.RunEndpoint{ID: "run-local", DisplayName: "Local Run", Version: "0.1.0", Capabilities: []string{"process.start", domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunDBSQLiteProbe}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||
if err != nil {
|
||||
t.Fatalf("create run endpoint: %v", err)
|
||||
}
|
||||
if _, err := core.CreateUser(domain.User{ID: "scum-probe-owner", DisplayName: "SCUM Probe Owner", Email: "scum-probe-owner@example.test", Status: domain.UserStatusActive, Roles: []string{"server-owner"}, PasswordHash: "secret-password"}); err != nil {
|
||||
t.Fatalf("create owner: %v", err)
|
||||
}
|
||||
auth, err := core.LoginUser(domain.UserLogin{Account: "scum-probe-owner@example.test", Password: "secret-password"})
|
||||
if err != nil {
|
||||
t.Fatalf("login: %v", err)
|
||||
}
|
||||
instance, err := core.CreateServerInstance(domain.ServerInstance{ID: "server-scum-probe-api", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "SCUM Probe API", OwnerUserID: "scum-probe-owner", State: domain.ServerInstanceStateRunning, ConfigVersion: 1})
|
||||
if err != nil {
|
||||
t.Fatalf("create server: %v", err)
|
||||
}
|
||||
if _, err := core.UpdateServerRuntimeBindingForSession(auth.SessionID, instance.ID, domain.RuntimeBindingUpdate{ProfileKey: "local", Bindings: map[string]string{}}); err != nil {
|
||||
t.Fatalf("create runtime binding: %v", err)
|
||||
}
|
||||
|
||||
router := NewAuthorizedRouterWithCore(core)
|
||||
unauthorized := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/"+instance.ID+"/scum/schema-probe", map[string]string{"idempotencyKey": "probe-api-denied"}, "")
|
||||
assertStatus(t, unauthorized, http.StatusUnauthorized)
|
||||
capabilities := requestWithAuth(t, router, http.MethodGet, "/api/v1/server-instances/"+instance.ID+"/scum/capabilities", ``, auth.SessionID)
|
||||
assertStatus(t, capabilities, http.StatusOK)
|
||||
capabilityBody := capabilities.Body.String()
|
||||
if !strings.Contains(capabilityBody, "schema-probe") || !strings.Contains(capabilityBody, "probeExecutorAvailable") {
|
||||
t.Fatalf("capability negotiation response missing SCUM gates: %s", capabilityBody)
|
||||
}
|
||||
for _, forbidden := range []string{"SCUM.db", "sqlite_master", "SELECT", "C:\\", "secret://", "password"} {
|
||||
if strings.Contains(capabilityBody, forbidden) {
|
||||
t.Fatalf("capability negotiation response leaked forbidden material %q: %s", forbidden, capabilityBody)
|
||||
}
|
||||
}
|
||||
recorder := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/"+instance.ID+"/scum/schema-probe", map[string]string{"idempotencyKey": "probe-api-current"}, auth.SessionID)
|
||||
assertStatus(t, recorder, http.StatusAccepted)
|
||||
body := recorder.Body.String()
|
||||
for _, forbidden := range []string{"SCUM.db", "sqlite_master", "SELECT", "C:\\", "secret://", "password"} {
|
||||
if strings.Contains(body, forbidden) {
|
||||
t.Fatalf("schema probe dispatch response leaked forbidden material %q: %s", forbidden, body)
|
||||
}
|
||||
}
|
||||
jobs, err := core.ListJobsForSession(auth.SessionID, domain.JobFilter{ServerInstanceID: instance.ID})
|
||||
if err != nil {
|
||||
t.Fatalf("list jobs: %v", err)
|
||||
}
|
||||
if len(jobs) != 1 || jobs[0].Capability != domain.JobCapabilityRemoteRunDBSQLiteProbe || jobs[0].TargetKey != "databases/scum-database" || jobs[0].InputRef != "" || len(jobs[0].ExecutionInput.Inputs) != 0 || jobs[0].ExecutionInput.RemoteAdapterKey != "" || jobs[0].ExecutionInput.RemoteAdapterKind != "" {
|
||||
t.Fatalf("schema probe endpoint did not queue fenced durable job: %+v", jobs)
|
||||
}
|
||||
if jobs[0].ExecutionInput.SQLiteSchemaProbe == nil || jobs[0].ExecutionInput.SQLiteSchemaProbe.RequestID != jobs[0].ID || jobs[0].ExecutionInput.SQLiteSchemaProbe.Binding.DatabaseIdentity != "scum-database" || jobs[0].ExecutionInput.SQLiteSchemaProbe.Bounds.MaxResultBytes != 524288 {
|
||||
t.Fatalf("schema probe endpoint did not attach typed probe request: %+v", jobs[0].ExecutionInput.SQLiteSchemaProbe)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacySCUMEndpointsReturnNotFoundWithoutDispatchingJobs(t *testing.T) {
|
||||
func TestSCUMProjectionOperationAndWorkflowAPIsExposeSafeTypedSurfaces(t *testing.T) {
|
||||
store := repo.NewMemoryStore()
|
||||
core := service.NewCoreService(store)
|
||||
if _, err := core.CreateUser(domain.User{ID: "scum-api-owner", DisplayName: "SCUM API Owner", Email: "scum-api-owner@example.test", Status: domain.UserStatusActive, Roles: []string{"server-owner"}, PasswordHash: "secret-password"}); err != nil {
|
||||
t.Fatalf("create owner: %v", err)
|
||||
}
|
||||
if _, err := core.CreateGamePlugin(validGamePluginRequest().ToDomain()); err != nil {
|
||||
plugin := validGamePluginRequest().ToDomain()
|
||||
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.game-client.command", "server.game-client.read")
|
||||
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, domain.JobCapabilityRemoteRunRCONCommand, domain.JobCapabilityRemoteRunProtectedRCON)
|
||||
plugin.RuntimeProfiles.TransportProfiles = []domain.RuntimeTransportProfile{{Key: "scum-management", Kind: "rcon", TargetKey: "scum-management", Capabilities: []string{domain.JobCapabilityRemoteRunRCONCommand, domain.JobCapabilityRemoteRunProtectedRCON}}}
|
||||
plugin.GameClientBridge.OperationTemplates = []domain.GameClientBridgeOperationTemplateDeclaration{{Key: "player.fame.set", Title: "Set fame", Permission: "server.game-client.command", ApprovalLevel: domain.GameClientBridgeApprovalLevelOperator, Kind: domain.GameClientBridgeOperationKindRCON, TransportKey: "scum-management", TargetKey: "scum-management", PayloadSchemaRef: "schemas/bridge/player-fame-set.payload.schema.json", TimeoutSeconds: 60, MaxPayloadBytes: 2048, Safety: domain.GameClientBridgeOperationSafety{RequiresApproval: true, RequiresConfirmation: true}}}
|
||||
plugin.GameClientBridge.Retention = domain.GameClientBridgeRetention{KeepForSeconds: 86400, MaxRecords: 1000}
|
||||
if _, err := core.CreateGamePlugin(plugin); err != nil {
|
||||
t.Fatalf("create plugin: %v", err)
|
||||
}
|
||||
if _, err := core.CreateRunEndpoint(validRunEndpointRequest().ToDomain()); err != nil {
|
||||
endpoint := validRunEndpointRequest().ToDomain()
|
||||
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand, domain.JobCapabilityRemoteRunProtectedRCON)
|
||||
endpoint.LastHeartbeatAt = time.Now().UTC()
|
||||
if _, err := core.CreateRunEndpoint(endpoint); err != nil {
|
||||
t.Fatalf("create endpoint: %v", err)
|
||||
}
|
||||
if _, err := core.CreateServerInstance(domain.ServerInstance{ID: "server-scum-api", PluginID: "server.scum", RunEndpointID: "run-local", Name: "SCUM API", OwnerUserID: "scum-api-owner", State: domain.ServerInstanceStateRunning, ConfigVersion: 1}); err != nil {
|
||||
if _, err := core.CreateServerInstance(domain.ServerInstance{ID: "server-scum-api", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "SCUM API", OwnerUserID: "scum-api-owner", State: domain.ServerInstanceStateRunning, ConfigVersion: 1}); err != nil {
|
||||
t.Fatalf("create server: %v", err)
|
||||
}
|
||||
if _, err := core.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: "server-scum-api", PluginID: plugin.ID, Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 1, Checksum: "sha256:api-profile", ObservedAt: time.Now().UTC(), Rows: []map[string]any{{"gamePlayerId": "steam-api", "displayName": "API Player", "normalBalance": 25, "x": 1, "y": 2, "z": 3}}}); err != nil {
|
||||
t.Fatalf("seed projection: %v", err)
|
||||
}
|
||||
auth, err := core.LoginUser(domain.UserLogin{Account: "scum-api-owner@example.test", Password: "secret-password"})
|
||||
if err != nil {
|
||||
t.Fatalf("login: %v", err)
|
||||
}
|
||||
router := NewAuthorizedRouterWithCore(core)
|
||||
for _, legacy := range []struct{ method, path string }{
|
||||
{http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/players"}, {http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/squads"}, {http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/squad-members"}, {http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/vehicles"}, {http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/flags"}, {http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/positions"},
|
||||
{http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/operations"}, {http.MethodPost, "/api/v1/server-instances/server-scum-api/scum/operations"}, {http.MethodPost, "/api/v1/server-instances/server-scum-api/scum/operations/op-1/approve"}, {http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/workflows"}, {http.MethodPost, "/api/v1/server-instances/server-scum-api/scum/workflows"}, {http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/workflow-steps?workflowId=workflow-1"},
|
||||
} {
|
||||
assertStatus(t, requestWithAuth(t, router, legacy.method, legacy.path, `{}`, auth.SessionID), http.StatusNotFound)
|
||||
players := getJSONWithAuth[dto.SCUMPlayerLiveStateListResponse](t, router, "/api/v1/server-instances/server-scum-api/scum/players", auth.SessionID)
|
||||
if players.Count != 1 || players.Items[0].GamePlayerID != "steam-api" || players.Items[0].Position.X != 1 {
|
||||
t.Fatalf("unexpected SCUM players response: %+v", players)
|
||||
}
|
||||
jobs, err := core.ListJobsForSession(auth.SessionID, domain.JobFilter{ServerInstanceID: "server-scum-api"})
|
||||
if err != nil || len(jobs) != 0 {
|
||||
t.Fatalf("removed SCUM endpoints must not dispatch jobs, got jobs=%+v err=%v", jobs, err)
|
||||
operation := postJSONWithAuth[dto.SCUMOperationResponse](t, router, "/api/v1/server-instances/server-scum-api/scum/operations", dto.SCUMOperationRequestBody{TemplateKey: "player.fame.set", PlayerID: "steam-api", Payload: map[string]any{"fame": 12}, Reason: "api typed op", IdempotencyKey: "api-fame-1"}, auth.SessionID)
|
||||
if operation.Status != string(domain.SCUMWorkflowStepWaiting) || operation.TemplateKey != "player.fame.set" {
|
||||
t.Fatalf("unexpected SCUM operation response: %+v", operation)
|
||||
}
|
||||
operations := getJSONWithAuth[dto.SCUMOperationListResponse](t, router, "/api/v1/server-instances/server-scum-api/scum/operations", auth.SessionID)
|
||||
if operations.Count != 1 || operations.Items[0].ID != operation.ID {
|
||||
t.Fatalf("unexpected SCUM operation list: %+v", operations)
|
||||
}
|
||||
workflow := postJSONWithAuth[dto.SCUMWorkflowResponse](t, router, "/api/v1/server-instances/server-scum-api/scum/workflows", dto.SCUMWorkflowCreateRequest{TemplateKey: "scum.world-refresh", IdempotencyKey: "api-world-1"}, auth.SessionID)
|
||||
if workflow.Status != string(domain.SCUMWorkflowQueued) || workflow.TemplateKey != "scum.world-refresh" {
|
||||
t.Fatalf("unexpected SCUM workflow response: %+v", workflow)
|
||||
}
|
||||
steps := getJSONWithAuth[dto.SCUMWorkflowStepListResponse](t, router, "/api/v1/server-instances/server-scum-api/scum/workflow-steps?workflowId="+workflow.ID, auth.SessionID)
|
||||
if steps.Count == 0 {
|
||||
t.Fatalf("expected workflow steps: %+v", steps)
|
||||
}
|
||||
body, err := json.Marshal([]any{players, operation, operations, workflow, steps})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal responses: %v", err)
|
||||
}
|
||||
for _, forbidden := range []string{"#SetFamePoints", "requestText", "SELECT ", "UPDATE ", "SCUM.db", "password", "run token", "hostPath"} {
|
||||
if strings.Contains(strings.ToUpper(string(body)), strings.ToUpper(forbidden)) {
|
||||
t.Fatalf("SCUM safe API leaked %q: %s", forbidden, body)
|
||||
}
|
||||
}
|
||||
for _, legacy := range []struct{ method, path string }{
|
||||
{http.MethodPost, "/api/v1/server-instances/server-scum-api/rcon/commands"},
|
||||
{http.MethodGet, "/api/v1/server-instances/server-scum-api/logs/live"},
|
||||
{http.MethodPost, "/api/v1/server-instances/server-scum-api/logs/backfill"},
|
||||
{http.MethodGet, "/api/v1/server-instances/server-scum-api/files/read-snapshot?key=scum-server-log"},
|
||||
{http.MethodGet, "/api/v1/server-instances/server-scum-api/config"},
|
||||
{http.MethodPost, "/api/v1/server-instances/server-scum-api/config/diff"},
|
||||
{http.MethodPost, "/api/v1/server-instances/server-scum-api/config/approve"},
|
||||
} {
|
||||
recorder := requestWithAuth(t, router, legacy.method, legacy.path, `{}`, auth.SessionID)
|
||||
assertStatus(t, recorder, http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMAPIsEnforceServerAuthorization(t *testing.T) {
|
||||
store := repo.NewMemoryStore()
|
||||
core := service.NewCoreService(store)
|
||||
if _, err := core.CreateUser(domain.User{ID: "scum-api-owner", DisplayName: "SCUM API Owner", Email: "scum-api-owner-authz@example.test", Status: domain.UserStatusActive, Roles: []string{"server-owner"}, PasswordHash: "secret-password"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := core.CreateUser(domain.User{ID: "scum-api-other", DisplayName: "SCUM API Other", Email: "scum-api-other-authz@example.test", Status: domain.UserStatusActive, Roles: []string{"server-owner"}, PasswordHash: "secret-password"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := core.CreateGamePlugin(validGamePluginRequest().ToDomain()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := core.CreateRunEndpoint(validRunEndpointRequest().ToDomain()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := core.CreateServerInstance(domain.ServerInstance{ID: "server-scum-authz", PluginID: "server.scum", RunEndpointID: "run-local", Name: "SCUM Authz", OwnerUserID: "scum-api-owner", State: domain.ServerInstanceStateRunning}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
auth, err := core.LoginUser(domain.UserLogin{Account: "scum-api-other-authz@example.test", Password: "secret-password"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
router := NewAuthorizedRouterWithCore(core)
|
||||
assertErrorResponse(t, requestWithAuth(t, router, http.MethodGet, "/api/v1/server-instances/server-scum-authz/scum/players", "", auth.SessionID), http.StatusForbidden, errorCodeForbidden)
|
||||
}
|
||||
|
||||
func TestSCUMAPIsRequirePlatformAdminForDBMutationApproval(t *testing.T) {
|
||||
store := repo.NewMemoryStore()
|
||||
core := service.NewCoreService(store)
|
||||
if _, err := core.CreateUser(domain.User{ID: "scum-api-owner", DisplayName: "SCUM API Owner", Email: "scum-api-owner-mutation@example.test", Status: domain.UserStatusActive, Roles: []string{"server-owner"}, PasswordHash: "secret-password"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plugin := validGamePluginRequest().ToDomain()
|
||||
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.game-client.read", "server.game-client.maintenance")
|
||||
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunProtectedSQL)
|
||||
plugin.RuntimeProfiles.TransportProfiles = []domain.RuntimeTransportProfile{{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunProtectedSQL}}}
|
||||
plugin.GameClientBridge.QueryTemplates = []domain.GameClientBridgeQueryTemplateDeclaration{{Key: "scum.player.profile", Title: "Read player profile", Permission: "server.game-client.read", Engine: "sqlite", TransportKey: "scum-database", TargetKey: "scum-database", ParameterSchemaRef: "schemas/bridge/queries/scum-player-profile.parameters.schema.json", ResultSchemaRef: "schemas/bridge/queries/scum-player-profile.result.schema.json", MaxRows: 10, TimeoutSeconds: 15}}
|
||||
plugin.GameClientBridge.OperationTemplates = []domain.GameClientBridgeOperationTemplateDeclaration{{Key: "player.attribute.855.set", Title: "Set attribute 855", Permission: "server.game-client.maintenance", ApprovalLevel: domain.GameClientBridgeApprovalLevelPlatformAdmin, Kind: domain.GameClientBridgeOperationKindSQLiteMutation, TransportKey: "scum-database", TargetKey: "scum-database", PayloadSchemaRef: "schemas/bridge/player-attribute-855-set.payload.schema.json", ResultSchemaRef: "schemas/bridge/player-attribute-855-set.result.schema.json", ConfirmationSchemaRef: "schemas/bridge/player-attribute-855-set.confirmation.schema.json", TimeoutSeconds: 120, MaxPayloadBytes: 4096, MaxRowsAffected: 1, Mutation: domain.GameClientBridgeOperationMutationDeclaration{FieldKey: "855", TableKey: "prisoner", IdentityKey: "user_profile_id", ValueKey: "value", ConfirmationQueryKey: "scum.player.profile", AllowedValueType: "integer", MinValue: 0, MaxValue: 100000}, Safety: domain.GameClientBridgeOperationSafety{RequiresApproval: true, RequiresOfflinePlayer: true, RequiresMaintenanceWindow: true, RequiresBeforeValue: true, RequiresConfirmation: true, BackupRequired: true}}}
|
||||
plugin.GameClientBridge.Retention = domain.GameClientBridgeRetention{KeepForSeconds: 86400, MaxRecords: 1000}
|
||||
if _, err := core.CreateGamePlugin(plugin); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
endpoint := validRunEndpointRequest().ToDomain()
|
||||
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunProtectedSQL)
|
||||
if _, err := core.CreateRunEndpoint(endpoint); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := core.CreateServerInstance(domain.ServerInstance{ID: "server-scum-mutation-authz", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "SCUM Mutation Authz", OwnerUserID: "scum-api-owner", State: domain.ServerInstanceStateStopped}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
auth, err := core.LoginUser(domain.UserLogin{Account: "scum-api-owner-mutation@example.test", Password: "secret-password"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
router := NewAuthorizedRouterWithCore(core)
|
||||
operation := postJSONWithAuth[dto.SCUMOperationResponse](t, router, "/api/v1/server-instances/server-scum-mutation-authz/scum/operations", dto.SCUMOperationRequestBody{TemplateKey: "player.attribute.855.set", PlayerID: "steam-api", Payload: map[string]any{"fieldKey": "855", "before": 10, "after": 12, "safetyWindow": "maintenance-2026-08-10", "backupRef": "backup://scum/1"}, Reason: "api typed db op", IdempotencyKey: "api-855-1"}, auth.SessionID)
|
||||
assertErrorResponse(t, requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/server-scum-mutation-authz/scum/operations/"+operation.ID+"/approve", map[string]string{}, auth.SessionID), http.StatusForbidden, errorCodeForbidden)
|
||||
}
|
||||
|
||||
@@ -148,7 +148,6 @@ type RunAutonomousLifecyclePlan struct {
|
||||
InstallPlans []RunAutonomousInstallPlan `json:"installPlans,omitempty"`
|
||||
LogSources []RunAutonomousLogSource `json:"logSources,omitempty"`
|
||||
DLLExtensions []RunAutonomousDLLExtension `json:"dllExtensions,omitempty"`
|
||||
DataTargets []RunAutonomousDataTarget `json:"dataTargets,omitempty"`
|
||||
RuntimeBindings map[string]string `json:"runtimeBindings,omitempty"`
|
||||
Deployment *RunAutonomousDeployment `json:"deployment,omitempty"`
|
||||
}
|
||||
@@ -209,18 +208,6 @@ type RunAutonomousDLLExtension struct {
|
||||
RCONPort int `json:"rconPort,omitempty"`
|
||||
}
|
||||
|
||||
type RunAutonomousDataTarget struct {
|
||||
Key string `json:"key"`
|
||||
Kind string `json:"kind"`
|
||||
TransportKey string `json:"transportKey"`
|
||||
SourceRootKey string `json:"sourceRootKey"`
|
||||
SourcePath string `json:"sourcePath"`
|
||||
WorkspaceKey string `json:"workspaceKey"`
|
||||
RefreshPolicy string `json:"refreshPolicy"`
|
||||
MaxBytes int64 `json:"maxBytes,omitempty"`
|
||||
Platforms []string `json:"platforms,omitempty"`
|
||||
}
|
||||
|
||||
type RunAutonomousDeployment struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
Mode ServerDeploymentMode `json:"mode"`
|
||||
@@ -435,10 +422,6 @@ func CopyRunJobAssignment(assignment RunJobAssignment) RunJobAssignment {
|
||||
assignment.ExecutionInput.LogSources = CopyRuntimeLogSources(assignment.ExecutionInput.LogSources)
|
||||
assignment.ExecutionInput.DLLExtensions = append([]RuntimeDLLExtensionPlan(nil), assignment.ExecutionInput.DLLExtensions...)
|
||||
assignment.ExecutionInput.SourceRCON = CopyRuntimeSourceRCONPlan(assignment.ExecutionInput.SourceRCON)
|
||||
assignment.ExecutionInput.SQLiteSchemaProbe = CopySCUMSchemaProbeRequestPtr(assignment.ExecutionInput.SQLiteSchemaProbe)
|
||||
assignment.ExecutionInput.SQLiteTemplate = CopySCUMSQLiteTemplateRequestPtr(assignment.ExecutionInput.SQLiteTemplate)
|
||||
assignment.ExecutionInput.RCONTemplate = CopySCUMTypedRCONTemplateRequestPtr(assignment.ExecutionInput.RCONTemplate)
|
||||
assignment.ExecutionInput.GuardedMutation = CopySCUMGuardedMutationRequestPtr(assignment.ExecutionInput.GuardedMutation)
|
||||
return assignment
|
||||
}
|
||||
|
||||
@@ -513,10 +496,6 @@ func CopyRunAutonomousLifecyclePlanPtr(plan *RunAutonomousLifecyclePlan) *RunAut
|
||||
}
|
||||
copy.LogSources = append([]RunAutonomousLogSource(nil), plan.LogSources...)
|
||||
copy.DLLExtensions = append([]RunAutonomousDLLExtension(nil), plan.DLLExtensions...)
|
||||
copy.DataTargets = append([]RunAutonomousDataTarget(nil), plan.DataTargets...)
|
||||
for i := range copy.DataTargets {
|
||||
copy.DataTargets[i].Platforms = CopyStringSlice(plan.DataTargets[i].Platforms)
|
||||
}
|
||||
copy.RuntimeBindings = CopyStringMap(plan.RuntimeBindings)
|
||||
if plan.Deployment != nil {
|
||||
deployment := *plan.Deployment
|
||||
|
||||
@@ -87,17 +87,15 @@ type RemoteAdapterDeclaration struct {
|
||||
}
|
||||
|
||||
type RemoteAdapterRequest struct {
|
||||
ServerInstanceID string
|
||||
DeclarationKey string
|
||||
TargetKey string
|
||||
Capability string
|
||||
TimeoutSeconds int
|
||||
MaxAttempts int
|
||||
IdempotencyKey string
|
||||
InputRef string
|
||||
Inputs map[string]string
|
||||
PlatformScheduled bool
|
||||
SQLiteSchemaProbe *SCUMSchemaProbeRequest
|
||||
ServerInstanceID string
|
||||
DeclarationKey string
|
||||
TargetKey string
|
||||
Capability string
|
||||
TimeoutSeconds int
|
||||
MaxAttempts int
|
||||
IdempotencyKey string
|
||||
InputRef string
|
||||
Inputs map[string]string
|
||||
}
|
||||
|
||||
type RemoteAdapterResult struct {
|
||||
@@ -213,7 +211,6 @@ func CopyRemoteAdapterDeclarations(declarations []RemoteAdapterDeclaration) []Re
|
||||
|
||||
func CopyRemoteAdapterRequest(request RemoteAdapterRequest) RemoteAdapterRequest {
|
||||
request.Inputs = CopyStringMap(request.Inputs)
|
||||
request.SQLiteSchemaProbe = CopySCUMSchemaProbeRequestPtr(request.SQLiteSchemaProbe)
|
||||
return request
|
||||
}
|
||||
func CopyRemoteAdapterResult(result RemoteAdapterResult) RemoteAdapterResult { return result }
|
||||
|
||||
@@ -479,18 +479,6 @@ type RuntimeTransportProfile struct {
|
||||
Capabilities []string
|
||||
}
|
||||
|
||||
type RuntimeDataTarget struct {
|
||||
Key string
|
||||
Kind string
|
||||
TransportKey string
|
||||
SourceRootKey string
|
||||
SourcePath string
|
||||
WorkspaceKey string
|
||||
RefreshPolicy string
|
||||
MaxBytes int64
|
||||
Platforms []string
|
||||
}
|
||||
|
||||
type RuntimeClientManagerProfile struct {
|
||||
Key string
|
||||
DisplayName string
|
||||
@@ -621,7 +609,6 @@ type GamePluginRuntimeProfiles struct {
|
||||
LogSources []RuntimeLogSource
|
||||
LogEvents []RuntimeLogEvent
|
||||
TransportProfiles []RuntimeTransportProfile
|
||||
DataTargets []RuntimeDataTarget
|
||||
ClientManagers []RuntimeClientManagerProfile
|
||||
DLLExtensions []RuntimeDLLExtensionProfile
|
||||
}
|
||||
@@ -646,7 +633,6 @@ type GamePluginManifest struct {
|
||||
RemoteAccess GamePluginRemoteAccess
|
||||
RuntimeProfiles GamePluginRuntimeProfiles
|
||||
GameClientBridge GameClientBridgeManifest
|
||||
SCUMLiveData SCUMLiveDataManifest
|
||||
MapTrajectories *GameMapTrajectoryDeclaration
|
||||
}
|
||||
|
||||
@@ -687,7 +673,6 @@ type GamePlugin struct {
|
||||
RemoteAccess GamePluginRemoteAccess
|
||||
RuntimeProfiles GamePluginRuntimeProfiles
|
||||
GameClientBridge GameClientBridgeManifest
|
||||
SCUMLiveData SCUMLiveDataManifest
|
||||
MapTrajectories *GameMapTrajectoryDeclaration
|
||||
ValidationViolations []string
|
||||
Status GamePluginStatus
|
||||
@@ -1093,7 +1078,6 @@ const (
|
||||
JobCapabilityRemoteRunProcessStart = "remote.run.process.start"
|
||||
JobCapabilityRemoteRunProcessStop = "remote.run.process.stop"
|
||||
JobCapabilityRemoteRunDBMySQLQuery = "remote.run.db.mysql.query"
|
||||
JobCapabilityRemoteRunDBSQLiteProbe = "remote.run.db.sqlite.probe"
|
||||
JobCapabilityRemoteRunDBSQLiteQuery = "remote.run.db.sqlite.query"
|
||||
JobCapabilityRemoteRunLogsTransfer = "remote.run.logs.transfer"
|
||||
JobCapabilityRemoteRunRCONCommand = "remote.run.rcon.command"
|
||||
@@ -1156,10 +1140,6 @@ type JobExecutionInput struct {
|
||||
SourceRCON *RuntimeSourceRCONPlan
|
||||
Deployment *ServerDeploymentDefinition
|
||||
ServerDeploymentPlan *ServerDeploymentPlan
|
||||
SQLiteSchemaProbe *SCUMSchemaProbeRequest
|
||||
SQLiteTemplate *SCUMSQLiteTemplateRequest
|
||||
RCONTemplate *SCUMTypedRCONTemplateRequest
|
||||
GuardedMutation *SCUMGuardedMutationRequest
|
||||
}
|
||||
|
||||
type ServerDeploymentPlan struct {
|
||||
@@ -1202,11 +1182,6 @@ type JobExecutionResult struct {
|
||||
SizeBytes int64
|
||||
AuditSummary string
|
||||
Content string
|
||||
SQLiteSchemaProbe *SCUMSchemaProbeResult
|
||||
SQLiteTemplate *SCUMSQLiteTemplateResult
|
||||
RCONTemplate *SCUMTypedRCONTemplateResult
|
||||
GuardedMutation *SCUMGuardedMutationResult
|
||||
ParsedLogBatch *SCUMParsedLogBatchResult
|
||||
ServerDeploymentEvidence *ServerDeploymentEvidence
|
||||
DeploymentReceipt *ServerDeploymentExecutionReceipt
|
||||
}
|
||||
@@ -1719,7 +1694,6 @@ func CopyGamePlugin(plugin GamePlugin) GamePlugin {
|
||||
plugin.RemoteAccess = CopyGamePluginRemoteAccess(plugin.RemoteAccess)
|
||||
plugin.RuntimeProfiles = CopyGamePluginRuntimeProfiles(plugin.RuntimeProfiles)
|
||||
plugin.GameClientBridge = CopyGameClientBridgeManifest(plugin.GameClientBridge)
|
||||
plugin.SCUMLiveData = CopySCUMLiveDataManifest(plugin.SCUMLiveData)
|
||||
if plugin.MapTrajectories != nil {
|
||||
value := CopyGameMapTrajectoryDeclaration(*plugin.MapTrajectories)
|
||||
plugin.MapTrajectories = &value
|
||||
@@ -1790,7 +1764,6 @@ func CopyGamePluginManifest(manifest GamePluginManifest) GamePluginManifest {
|
||||
manifest.RemoteAccess = CopyGamePluginRemoteAccess(manifest.RemoteAccess)
|
||||
manifest.RuntimeProfiles = CopyGamePluginRuntimeProfiles(manifest.RuntimeProfiles)
|
||||
manifest.GameClientBridge = CopyGameClientBridgeManifest(manifest.GameClientBridge)
|
||||
manifest.SCUMLiveData = CopySCUMLiveDataManifest(manifest.SCUMLiveData)
|
||||
if manifest.MapTrajectories != nil {
|
||||
value := CopyGameMapTrajectoryDeclaration(*manifest.MapTrajectories)
|
||||
manifest.MapTrajectories = &value
|
||||
@@ -1853,10 +1826,6 @@ func CopyGamePluginRuntimeProfiles(profiles GamePluginRuntimeProfiles) GamePlugi
|
||||
for i := range profiles.TransportProfiles {
|
||||
profiles.TransportProfiles[i].Capabilities = CopyStringSlice(profiles.TransportProfiles[i].Capabilities)
|
||||
}
|
||||
profiles.DataTargets = append([]RuntimeDataTarget(nil), profiles.DataTargets...)
|
||||
for i := range profiles.DataTargets {
|
||||
profiles.DataTargets[i].Platforms = CopyStringSlice(profiles.DataTargets[i].Platforms)
|
||||
}
|
||||
profiles.ClientManagers = append([]RuntimeClientManagerProfile(nil), profiles.ClientManagers...)
|
||||
for i := range profiles.ClientManagers {
|
||||
profiles.ClientManagers[i].SupportedTargets = append([]RuntimeTarget(nil), profiles.ClientManagers[i].SupportedTargets...)
|
||||
@@ -2051,15 +2020,6 @@ func CopyJob(job Job) Job {
|
||||
job.ExecutionInput.DLLExtensions = append([]RuntimeDLLExtensionPlan(nil), job.ExecutionInput.DLLExtensions...)
|
||||
job.ExecutionInput.SourceRCON = CopyRuntimeSourceRCONPlan(job.ExecutionInput.SourceRCON)
|
||||
job.ExecutionInput.ServerDeploymentPlan = CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan)
|
||||
job.ExecutionInput.SQLiteSchemaProbe = CopySCUMSchemaProbeRequestPtr(job.ExecutionInput.SQLiteSchemaProbe)
|
||||
job.ExecutionInput.SQLiteTemplate = CopySCUMSQLiteTemplateRequestPtr(job.ExecutionInput.SQLiteTemplate)
|
||||
job.ExecutionInput.RCONTemplate = CopySCUMTypedRCONTemplateRequestPtr(job.ExecutionInput.RCONTemplate)
|
||||
job.ExecutionInput.GuardedMutation = CopySCUMGuardedMutationRequestPtr(job.ExecutionInput.GuardedMutation)
|
||||
job.ExecutionResult.SQLiteSchemaProbe = CopySCUMSchemaProbeResultPtr(job.ExecutionResult.SQLiteSchemaProbe)
|
||||
job.ExecutionResult.SQLiteTemplate = CopySCUMSQLiteTemplateResultPtr(job.ExecutionResult.SQLiteTemplate)
|
||||
job.ExecutionResult.RCONTemplate = CopySCUMTypedRCONTemplateResultPtr(job.ExecutionResult.RCONTemplate)
|
||||
job.ExecutionResult.GuardedMutation = CopySCUMGuardedMutationResultPtr(job.ExecutionResult.GuardedMutation)
|
||||
job.ExecutionResult.ParsedLogBatch = CopySCUMParsedLogBatchResultPtr(job.ExecutionResult.ParsedLogBatch)
|
||||
job.ExecutionResult.ServerDeploymentEvidence = CopyServerDeploymentEvidence(job.ExecutionResult.ServerDeploymentEvidence)
|
||||
job.ExecutionResult.DeploymentReceipt = CopyServerDeploymentExecutionReceipt(job.ExecutionResult.DeploymentReceipt)
|
||||
if job.ExecutionInput.Deployment != nil {
|
||||
|
||||
@@ -1,712 +0,0 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type SCUMDataCapability string
|
||||
|
||||
const (
|
||||
SCUMDataCapabilitySchemaProbe SCUMDataCapability = "schema-probe"
|
||||
SCUMDataCapabilityPlayerRead SCUMDataCapability = "players.read"
|
||||
SCUMDataCapabilityPlayerDetailRead SCUMDataCapability = "player-details.read"
|
||||
SCUMDataCapabilitySquadRead SCUMDataCapability = "squads.read"
|
||||
SCUMDataCapabilitySquadMemberRead SCUMDataCapability = "squad-members.read"
|
||||
SCUMDataCapabilityVehicleRead SCUMDataCapability = "vehicles.read"
|
||||
SCUMDataCapabilityFlagRead SCUMDataCapability = "flags.read"
|
||||
SCUMDataCapabilityPositionRead SCUMDataCapability = "positions.read"
|
||||
SCUMDataCapabilityProfileXMLWrite SCUMDataCapability = "profile-xml.write"
|
||||
SCUMDataCapabilityEconomyCommand SCUMDataCapability = "economy-command.write"
|
||||
SCUMDataCapabilityGiftCommand SCUMDataCapability = "gift-command.write"
|
||||
)
|
||||
|
||||
type SCUMCapabilityEvidenceStatus string
|
||||
|
||||
const (
|
||||
SCUMSchemaProbeStatusSucceeded SCUMCapabilityEvidenceStatus = "succeeded"
|
||||
SCUMCapabilityEvidenceMissing SCUMCapabilityEvidenceStatus = "missing"
|
||||
SCUMCapabilityEvidenceCompatible SCUMCapabilityEvidenceStatus = "compatible"
|
||||
SCUMCapabilityEvidenceIncompatible SCUMCapabilityEvidenceStatus = "incompatible"
|
||||
SCUMCapabilityEvidenceFailed SCUMCapabilityEvidenceStatus = "failed"
|
||||
)
|
||||
|
||||
type SCUMCapabilityGateState string
|
||||
|
||||
const (
|
||||
SCUMCapabilityGateEnabled SCUMCapabilityGateState = "enabled"
|
||||
SCUMCapabilityGateDisabled SCUMCapabilityGateState = "disabled"
|
||||
)
|
||||
|
||||
type SCUMSafeErrorCode string
|
||||
|
||||
const (
|
||||
SCUMSafeErrorNone SCUMSafeErrorCode = "none"
|
||||
SCUMSafeErrorProbeExecutorAbsent SCUMSafeErrorCode = "probe_executor_absent"
|
||||
SCUMSafeErrorProbeMissing SCUMSafeErrorCode = "probe_missing"
|
||||
SCUMSafeErrorProbeFailed SCUMSafeErrorCode = "probe_failed"
|
||||
SCUMSafeErrorSchemaIncompatible SCUMSafeErrorCode = "schema_incompatible"
|
||||
SCUMSafeErrorBindingMismatch SCUMSafeErrorCode = "binding_mismatch"
|
||||
SCUMSafeErrorAdapterMismatch SCUMSafeErrorCode = "adapter_mismatch"
|
||||
SCUMSafeErrorFingerprintMismatch SCUMSafeErrorCode = "fingerprint_mismatch"
|
||||
SCUMSafeErrorDigestMismatch SCUMSafeErrorCode = "digest_mismatch"
|
||||
SCUMSafeErrorEvidenceExpired SCUMSafeErrorCode = "evidence_expired"
|
||||
SCUMSafeErrorInvalidProbePayload SCUMSafeErrorCode = "invalid_probe_payload"
|
||||
SCUMSafeErrorInvalidRequest SCUMSafeErrorCode = "invalid_request"
|
||||
SCUMSafeErrorTargetUnavailable SCUMSafeErrorCode = "target_unavailable"
|
||||
SCUMSafeErrorSourceUnavailable SCUMSafeErrorCode = "source_unavailable"
|
||||
SCUMSafeErrorSQLiteOpenFailed SCUMSafeErrorCode = "sqlite_open_failed"
|
||||
SCUMSafeErrorSQLiteReadFailed SCUMSafeErrorCode = "sqlite_read_failed"
|
||||
SCUMSafeErrorDatabaseBusy SCUMSafeErrorCode = "database_busy"
|
||||
SCUMSafeErrorTimeout SCUMSafeErrorCode = "timeout"
|
||||
SCUMSafeErrorCancelled SCUMSafeErrorCode = "cancelled"
|
||||
SCUMSafeErrorSourceChanged SCUMSafeErrorCode = "source_changed"
|
||||
SCUMSafeErrorResultLimitExceeded SCUMSafeErrorCode = "result_limit_exceeded"
|
||||
SCUMSafeErrorTemplateMissing SCUMSafeErrorCode = "template_missing"
|
||||
SCUMSafeErrorTemplateMismatch SCUMSafeErrorCode = "template_digest_mismatch"
|
||||
SCUMSafeErrorParameterInvalid SCUMSafeErrorCode = "parameter_schema_invalid"
|
||||
SCUMSafeErrorRowLimitExceeded SCUMSafeErrorCode = "row_limit_exceeded"
|
||||
SCUMSafeErrorResultSchemaInvalid SCUMSafeErrorCode = "result_schema_invalid"
|
||||
SCUMSafeErrorMutationGuardMismatch SCUMSafeErrorCode = "mutation_guard_mismatch"
|
||||
SCUMSafeErrorMutationBackupUnavailable SCUMSafeErrorCode = "mutation_backup_unavailable"
|
||||
SCUMSafeErrorMutationOfflineRequired SCUMSafeErrorCode = "mutation_offline_required"
|
||||
SCUMSafeErrorMutationConfirmationMissing SCUMSafeErrorCode = "mutation_confirmation_missing"
|
||||
SCUMSafeErrorMutationPatchInvalid SCUMSafeErrorCode = "mutation_patch_invalid"
|
||||
SCUMSafeErrorAffectedRowsMismatch SCUMSafeErrorCode = "affected_rows_mismatch"
|
||||
SCUMSafeErrorReadbackMismatch SCUMSafeErrorCode = "readback_mismatch"
|
||||
SCUMSafeErrorRollbackFailed SCUMSafeErrorCode = "rollback_failed"
|
||||
)
|
||||
|
||||
type SCUMTerminalResultStatus string
|
||||
|
||||
const (
|
||||
SCUMTerminalResultSucceeded SCUMTerminalResultStatus = "succeeded"
|
||||
SCUMTerminalResultFailed SCUMTerminalResultStatus = "failed"
|
||||
SCUMTerminalResultCancelled SCUMTerminalResultStatus = "cancelled"
|
||||
)
|
||||
|
||||
type SCUMRCONConfirmationStatus string
|
||||
|
||||
const (
|
||||
SCUMRCONConfirmationConfirmed SCUMRCONConfirmationStatus = "confirmed"
|
||||
SCUMRCONConfirmationFailed SCUMRCONConfirmationStatus = "failed"
|
||||
SCUMRCONConfirmationUnknown SCUMRCONConfirmationStatus = "unknown"
|
||||
)
|
||||
|
||||
type SCUMMutationReadbackStatus string
|
||||
|
||||
const (
|
||||
SCUMMutationReadbackConfirmed SCUMMutationReadbackStatus = "confirmed"
|
||||
SCUMMutationReadbackFailed SCUMMutationReadbackStatus = "failed"
|
||||
SCUMMutationReadbackConflict SCUMMutationReadbackStatus = "conflict"
|
||||
SCUMMutationReadbackUnknown SCUMMutationReadbackStatus = "unknown"
|
||||
)
|
||||
|
||||
type SCUMSafeError struct {
|
||||
Code SCUMSafeErrorCode
|
||||
Message string
|
||||
Retryable bool
|
||||
}
|
||||
|
||||
type SCUMBindingIdentity struct {
|
||||
ServerInstanceID string
|
||||
RunBindingID string
|
||||
RunEndpointID string
|
||||
PluginID string
|
||||
PluginVersion string
|
||||
AdapterVersion string
|
||||
GameVersion string
|
||||
DatabaseIdentity string
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeBounds struct {
|
||||
MaxObjects int
|
||||
MaxColumnsPerObject int
|
||||
MaxIndexesPerObject int
|
||||
MaxForeignKeys int
|
||||
MaxCardinalityReads int
|
||||
MaxSampleRows int
|
||||
TimeoutMS int
|
||||
MaxResultBytes int
|
||||
}
|
||||
|
||||
func DefaultSCUMSchemaProbeBounds() SCUMSchemaProbeBounds {
|
||||
return SCUMSchemaProbeBounds{MaxObjects: 256, MaxColumnsPerObject: 128, MaxIndexesPerObject: 64, MaxForeignKeys: 64, MaxCardinalityReads: 64, MaxSampleRows: 3, TimeoutMS: 5000, MaxResultBytes: 512 * 1024}
|
||||
}
|
||||
|
||||
type SCUMSQLiteTemplateBounds struct {
|
||||
MaxParameters int
|
||||
MaxRows int
|
||||
TimeoutMS int
|
||||
BusyTimeoutMS int
|
||||
MaxResultBytes int
|
||||
}
|
||||
|
||||
func DefaultSCUMSQLiteTemplateBounds() SCUMSQLiteTemplateBounds {
|
||||
return SCUMSQLiteTemplateBounds{MaxParameters: 64, MaxRows: 500, TimeoutMS: 5000, BusyTimeoutMS: 250, MaxResultBytes: 1024 * 1024}
|
||||
}
|
||||
|
||||
type SCUMTypedRCONTemplateBounds struct {
|
||||
MaxPayloadBytes int
|
||||
TimeoutMS int
|
||||
MaxResponseBytes int
|
||||
MaxConfirmRecords int
|
||||
}
|
||||
|
||||
func DefaultSCUMTypedRCONTemplateBounds() SCUMTypedRCONTemplateBounds {
|
||||
return SCUMTypedRCONTemplateBounds{MaxPayloadBytes: 2048, TimeoutMS: 5000, MaxResponseBytes: 16 * 1024, MaxConfirmRecords: 16}
|
||||
}
|
||||
|
||||
type SCUMGuardedMutationBounds struct {
|
||||
MaxPayloadBytes int
|
||||
TimeoutMS int
|
||||
BusyTimeoutMS int
|
||||
MaxReadbackBytes int
|
||||
MaxAffectedRows int
|
||||
}
|
||||
|
||||
func DefaultSCUMGuardedMutationBounds() SCUMGuardedMutationBounds {
|
||||
return SCUMGuardedMutationBounds{MaxPayloadBytes: 4096, TimeoutMS: 5000, BusyTimeoutMS: 250, MaxReadbackBytes: 16 * 1024, MaxAffectedRows: 1}
|
||||
}
|
||||
|
||||
type SCUMParsedLogBatchBounds struct {
|
||||
MaxEvents int
|
||||
MaxPayloadBytes int
|
||||
MaxLineBytes int
|
||||
MaxResultBytes int
|
||||
}
|
||||
|
||||
func DefaultSCUMParsedLogBatchBounds() SCUMParsedLogBatchBounds {
|
||||
return SCUMParsedLogBatchBounds{MaxEvents: 256, MaxPayloadBytes: 16 * 1024, MaxLineBytes: 4096, MaxResultBytes: 256 * 1024}
|
||||
}
|
||||
|
||||
type SCUMLogTailState string
|
||||
|
||||
const (
|
||||
SCUMLogTailAdvanced SCUMLogTailState = "advanced"
|
||||
SCUMLogTailRotated SCUMLogTailState = "rotated"
|
||||
SCUMLogTailTruncated SCUMLogTailState = "truncated"
|
||||
SCUMLogTailRestarted SCUMLogTailState = "restarted"
|
||||
SCUMLogTailPartial SCUMLogTailState = "partial-buffered"
|
||||
SCUMLogTailReplayed SCUMLogTailState = "replayed"
|
||||
)
|
||||
|
||||
type SCUMParsedLogCursor struct {
|
||||
SourceIdentityDigest string
|
||||
StreamGeneration string
|
||||
Sequence uint64
|
||||
}
|
||||
|
||||
type SCUMParsedLogEvent struct {
|
||||
EventType string
|
||||
OccurredAt time.Time
|
||||
Cursor SCUMParsedLogCursor
|
||||
LogicalEventDigest string
|
||||
EventDigest string
|
||||
PayloadDigest string
|
||||
Payload map[string]any
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeRequest struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
Binding SCUMBindingIdentity
|
||||
Bounds SCUMSchemaProbeBounds
|
||||
RequestedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMSQLiteTemplateRequest struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
Binding SCUMBindingIdentity
|
||||
Capability SCUMDataCapability
|
||||
TargetKey string
|
||||
TemplateKey string
|
||||
AdapterVersion string
|
||||
RequiredSchemaFingerprint string
|
||||
AssetDigest string
|
||||
ParameterDigest string
|
||||
Parameters map[string]any
|
||||
Bounds SCUMSQLiteTemplateBounds
|
||||
RequestedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMTypedRCONTemplateRequest struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
Binding SCUMBindingIdentity
|
||||
Capability SCUMDataCapability
|
||||
TransportKey string
|
||||
TargetKey string
|
||||
TemplateKey string
|
||||
AdapterVersion string
|
||||
RequiredSchemaFingerprint string
|
||||
AssetDigest string
|
||||
PayloadDigest string
|
||||
ConfirmationDigest string
|
||||
TargetIdentityDigest string
|
||||
IdempotencyKey string
|
||||
Payload map[string]any
|
||||
ReviewReason string
|
||||
Bounds SCUMTypedRCONTemplateBounds
|
||||
RequestedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMGuardedMutationRequest struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
Binding SCUMBindingIdentity
|
||||
Capability SCUMDataCapability
|
||||
TargetKey string
|
||||
TemplateKey string
|
||||
AdapterVersion string
|
||||
RequiredSchemaFingerprint string
|
||||
AssetDigest string
|
||||
TargetIdentityDigest string
|
||||
ExpectedRowDigest string
|
||||
ExpectedValueDigest string
|
||||
ExpectedXMLDigest string
|
||||
PatchDigest string
|
||||
BackupEvidenceDigest string
|
||||
OfflineEvidenceDigest string
|
||||
DangerConfirmationDigest string
|
||||
ReadbackExpectationDigest string
|
||||
IdempotencyKey string
|
||||
Payload map[string]any
|
||||
ReviewReason string
|
||||
Bounds SCUMGuardedMutationBounds
|
||||
RequestedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeDeclaration struct {
|
||||
Capability string
|
||||
TargetKey string
|
||||
Bounds SCUMSchemaProbeBounds
|
||||
}
|
||||
|
||||
type SCUMLiveDataCapabilityGateDeclaration struct {
|
||||
Capability SCUMDataCapability
|
||||
Gate SCUMCapabilityGateState
|
||||
AdapterVersion string
|
||||
RequiredSchemaFingerprint string
|
||||
RequiredAssetDigests []string
|
||||
EvidenceStatus SCUMCapabilityEvidenceStatus
|
||||
SafeReason string
|
||||
}
|
||||
|
||||
type SCUMLiveDataManifest struct {
|
||||
SchemaVersion string
|
||||
Probe SCUMSchemaProbeDeclaration
|
||||
CapabilityGates []SCUMLiveDataCapabilityGateDeclaration
|
||||
}
|
||||
|
||||
type SCUMSchemaColumnEvidence struct {
|
||||
NameFingerprint string
|
||||
DeclaredType string
|
||||
Nullable *bool
|
||||
PrimaryKey bool
|
||||
Ordinal int
|
||||
}
|
||||
|
||||
type SCUMSchemaIndexEvidence struct {
|
||||
NameFingerprint string
|
||||
Unique bool
|
||||
ColumnHashes []string
|
||||
}
|
||||
|
||||
type SCUMSchemaForeignKeyEvidence struct {
|
||||
FromColumnHash string
|
||||
ToObjectHash string
|
||||
ToColumnHash string
|
||||
}
|
||||
|
||||
type SCUMSchemaObjectEvidence struct {
|
||||
ObjectHash string
|
||||
Kind string
|
||||
NameFingerprint string
|
||||
DeclaredColumns []SCUMSchemaColumnEvidence
|
||||
Indexes []SCUMSchemaIndexEvidence
|
||||
ForeignKeys []SCUMSchemaForeignKeyEvidence
|
||||
ApproximateRows *int64
|
||||
SampleFingerprints []string
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeResult struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
Binding SCUMBindingIdentity
|
||||
Status SCUMCapabilityEvidenceStatus
|
||||
SourceFingerprint string
|
||||
SchemaFingerprint string
|
||||
ObservedAt time.Time
|
||||
ResultDigest string
|
||||
Objects []SCUMSchemaObjectEvidence
|
||||
SafeError SCUMSafeError
|
||||
Limits SCUMSchemaProbeBounds
|
||||
}
|
||||
|
||||
type SCUMSQLiteTemplateResult struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
Binding SCUMBindingIdentity
|
||||
Status SCUMTerminalResultStatus
|
||||
Capability SCUMDataCapability
|
||||
TargetKey string
|
||||
TemplateKey string
|
||||
AdapterVersion string
|
||||
SchemaFingerprint string
|
||||
AssetDigest string
|
||||
ParameterDigest string
|
||||
SourceFingerprint string
|
||||
ObservedAt time.Time
|
||||
ResultDigest string
|
||||
RowCount int
|
||||
Rows []map[string]any
|
||||
Truncated bool
|
||||
SafeError SCUMSafeError
|
||||
Limits SCUMSQLiteTemplateBounds
|
||||
}
|
||||
|
||||
type SCUMTypedRCONTemplateResult struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
Binding SCUMBindingIdentity
|
||||
Status SCUMTerminalResultStatus
|
||||
Capability SCUMDataCapability
|
||||
TransportKey string
|
||||
TargetKey string
|
||||
TemplateKey string
|
||||
AdapterVersion string
|
||||
SchemaFingerprint string
|
||||
AssetDigest string
|
||||
PayloadDigest string
|
||||
ConfirmationDigest string
|
||||
TargetIdentityDigest string
|
||||
ObservedAt time.Time
|
||||
ResultDigest string
|
||||
ResponseDigest string
|
||||
ConfirmationStatus SCUMRCONConfirmationStatus
|
||||
ConfirmationDigestID string
|
||||
SafeSummary string
|
||||
SafeError SCUMSafeError
|
||||
Limits SCUMTypedRCONTemplateBounds
|
||||
}
|
||||
|
||||
type SCUMGuardedMutationResult struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
Binding SCUMBindingIdentity
|
||||
Status SCUMTerminalResultStatus
|
||||
Capability SCUMDataCapability
|
||||
TargetKey string
|
||||
TemplateKey string
|
||||
AdapterVersion string
|
||||
SchemaFingerprint string
|
||||
AssetDigest string
|
||||
SourceFingerprint string
|
||||
TargetIdentityDigest string
|
||||
ExpectedRowDigest string
|
||||
ExpectedValueDigest string
|
||||
ExpectedXMLDigest string
|
||||
PatchDigest string
|
||||
BackupEvidenceDigest string
|
||||
OfflineEvidenceDigest string
|
||||
DangerConfirmationDigest string
|
||||
ReadbackExpectationDigest string
|
||||
ObservedAt time.Time
|
||||
ResultDigest string
|
||||
BeforeDigest string
|
||||
AfterDigest string
|
||||
ReadbackDigest string
|
||||
AffectedRows int
|
||||
ReadbackStatus SCUMMutationReadbackStatus
|
||||
SafeSummary string
|
||||
SafeError SCUMSafeError
|
||||
Limits SCUMGuardedMutationBounds
|
||||
}
|
||||
|
||||
type SCUMParsedLogBatchResult struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
Binding SCUMBindingIdentity
|
||||
Status SCUMTerminalResultStatus
|
||||
SourceKey string
|
||||
StreamKey string
|
||||
ParserKey string
|
||||
ParserVersion string
|
||||
AdapterVersion string
|
||||
AssetDigest string
|
||||
ParserDigest string
|
||||
ObservedAt time.Time
|
||||
ResultDigest string
|
||||
FirstCursor SCUMParsedLogCursor
|
||||
LastCursor SCUMParsedLogCursor
|
||||
TailState SCUMLogTailState
|
||||
PartialLineBuffered bool
|
||||
Replay bool
|
||||
EventCount int
|
||||
Events []SCUMParsedLogEvent
|
||||
SafeSummary string
|
||||
SafeError SCUMSafeError
|
||||
Limits SCUMParsedLogBatchBounds
|
||||
}
|
||||
|
||||
type SCUMCapabilityRequirement struct {
|
||||
Capability SCUMDataCapability
|
||||
AdapterVersion string
|
||||
SchemaFingerprint string
|
||||
AssetDigests []string
|
||||
}
|
||||
|
||||
type SCUMCapabilityEvidence struct {
|
||||
Capability SCUMDataCapability
|
||||
Status SCUMCapabilityEvidenceStatus
|
||||
Binding SCUMBindingIdentity
|
||||
AdapterVersion string
|
||||
SchemaFingerprint string
|
||||
ProbeResultDigest string
|
||||
AssetDigests []string
|
||||
ObservedAt time.Time
|
||||
ExpiresAt time.Time
|
||||
SafeError SCUMSafeError
|
||||
}
|
||||
|
||||
type SCUMCapabilityGate struct {
|
||||
Capability SCUMDataCapability
|
||||
State SCUMCapabilityGateState
|
||||
Enabled bool
|
||||
ReasonCode SCUMSafeErrorCode
|
||||
Reason string
|
||||
Evidence SCUMCapabilityEvidence
|
||||
}
|
||||
|
||||
type SCUMCapabilityNegotiation struct {
|
||||
ServerInstanceID string
|
||||
RunEndpointID string
|
||||
RunBindingID string
|
||||
PluginID string
|
||||
PluginVersion string
|
||||
AdapterVersion string
|
||||
GameVersion string
|
||||
DatabaseIdentity string
|
||||
ProbeExecutorAvailable bool
|
||||
EvaluatedAt time.Time
|
||||
Gates []SCUMCapabilityGate
|
||||
}
|
||||
|
||||
func EvaluateSCUMCapabilityGate(requirement SCUMCapabilityRequirement, evidence SCUMCapabilityEvidence, active SCUMBindingIdentity, probeExecutorAvailable bool, now time.Time) SCUMCapabilityGate {
|
||||
gate := SCUMCapabilityGate{Capability: requirement.Capability, State: SCUMCapabilityGateDisabled, ReasonCode: SCUMSafeErrorProbeMissing, Reason: "current-service evidence is required before this SCUM capability can run"}
|
||||
if !probeExecutorAvailable {
|
||||
gate.ReasonCode = SCUMSafeErrorProbeExecutorAbsent
|
||||
gate.Reason = "bound Run does not expose the generic SQLite schema-probe executor"
|
||||
return gate
|
||||
}
|
||||
if evidence.Status == SCUMCapabilityEvidenceMissing || evidence.Capability == "" {
|
||||
return gate
|
||||
}
|
||||
gate.Evidence = CopySCUMCapabilityEvidence(evidence)
|
||||
if evidence.Status == SCUMCapabilityEvidenceFailed {
|
||||
gate.ReasonCode = SCUMSafeErrorProbeFailed
|
||||
gate.Reason = safeReason(evidence.SafeError.Message, "last schema probe failed")
|
||||
return gate
|
||||
}
|
||||
if evidence.Status == SCUMCapabilityEvidenceIncompatible {
|
||||
gate.ReasonCode = SCUMSafeErrorSchemaIncompatible
|
||||
gate.Reason = safeReason(evidence.SafeError.Message, "current schema is incompatible with the plugin adapter")
|
||||
return gate
|
||||
}
|
||||
if evidence.Capability != requirement.Capability {
|
||||
gate.ReasonCode = SCUMSafeErrorSchemaIncompatible
|
||||
gate.Reason = "capability evidence does not match the requested SCUM capability"
|
||||
return gate
|
||||
}
|
||||
if !sameSCUMBinding(evidence.Binding, active) {
|
||||
gate.ReasonCode = SCUMSafeErrorBindingMismatch
|
||||
gate.Reason = "evidence belongs to a different server, Run binding, plugin, adapter, game, or database identity"
|
||||
return gate
|
||||
}
|
||||
if evidence.AdapterVersion != requirement.AdapterVersion {
|
||||
gate.ReasonCode = SCUMSafeErrorAdapterMismatch
|
||||
gate.Reason = "evidence adapter version does not match the plugin requirement"
|
||||
return gate
|
||||
}
|
||||
if evidence.SchemaFingerprint == "" || evidence.SchemaFingerprint != requirement.SchemaFingerprint {
|
||||
gate.ReasonCode = SCUMSafeErrorFingerprintMismatch
|
||||
gate.Reason = "schema fingerprint does not match the plugin requirement"
|
||||
return gate
|
||||
}
|
||||
if !containsAllStrings(evidence.AssetDigests, requirement.AssetDigests) {
|
||||
gate.ReasonCode = SCUMSafeErrorDigestMismatch
|
||||
gate.Reason = "packaged asset digest does not match the compatible evidence"
|
||||
return gate
|
||||
}
|
||||
if !evidence.ExpiresAt.IsZero() && !now.IsZero() && !now.Before(evidence.ExpiresAt) {
|
||||
gate.ReasonCode = SCUMSafeErrorEvidenceExpired
|
||||
gate.Reason = "current-service evidence has expired and must be probed again"
|
||||
return gate
|
||||
}
|
||||
gate.State = SCUMCapabilityGateEnabled
|
||||
gate.Enabled = true
|
||||
gate.ReasonCode = SCUMSafeErrorNone
|
||||
gate.Reason = "current-service evidence matches the versioned plugin adapter"
|
||||
return gate
|
||||
}
|
||||
|
||||
func CopySCUMCapabilityEvidence(value SCUMCapabilityEvidence) SCUMCapabilityEvidence {
|
||||
value.AssetDigests = append([]string(nil), value.AssetDigests...)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMSQLiteTemplateRequestPtr(value *SCUMSQLiteTemplateRequest) *SCUMSQLiteTemplateRequest {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
copy.Parameters = CopySCUMValueMap(value.Parameters)
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMTypedRCONTemplateRequestPtr(value *SCUMTypedRCONTemplateRequest) *SCUMTypedRCONTemplateRequest {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
copy.Payload = CopySCUMValueMap(value.Payload)
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMGuardedMutationRequestPtr(value *SCUMGuardedMutationRequest) *SCUMGuardedMutationRequest {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
copy.Payload = CopySCUMValueMap(value.Payload)
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMSQLiteTemplateResultPtr(value *SCUMSQLiteTemplateResult) *SCUMSQLiteTemplateResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
copy.Rows = CopySCUMRows(value.Rows)
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMTypedRCONTemplateResultPtr(value *SCUMTypedRCONTemplateResult) *SCUMTypedRCONTemplateResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMGuardedMutationResultPtr(value *SCUMGuardedMutationResult) *SCUMGuardedMutationResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMParsedLogBatchResultPtr(value *SCUMParsedLogBatchResult) *SCUMParsedLogBatchResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
copy.Events = CopySCUMParsedLogEvents(value.Events)
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMParsedLogEvents(events []SCUMParsedLogEvent) []SCUMParsedLogEvent {
|
||||
if events == nil {
|
||||
return nil
|
||||
}
|
||||
copy := make([]SCUMParsedLogEvent, len(events))
|
||||
for index, event := range events {
|
||||
copy[index] = event
|
||||
copy[index].Payload = CopySCUMValueMap(event.Payload)
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
func CopySCUMValueMap(value map[string]any) map[string]any {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := make(map[string]any, len(value))
|
||||
for key, item := range value {
|
||||
copy[key] = item
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
func CopySCUMRows(rows []map[string]any) []map[string]any {
|
||||
if rows == nil {
|
||||
return nil
|
||||
}
|
||||
copy := make([]map[string]any, len(rows))
|
||||
for index, row := range rows {
|
||||
copy[index] = CopySCUMValueMap(row)
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
func CopySCUMSchemaProbeResult(value SCUMSchemaProbeResult) SCUMSchemaProbeResult {
|
||||
value.Objects = append([]SCUMSchemaObjectEvidence(nil), value.Objects...)
|
||||
for index := range value.Objects {
|
||||
value.Objects[index].DeclaredColumns = append([]SCUMSchemaColumnEvidence(nil), value.Objects[index].DeclaredColumns...)
|
||||
value.Objects[index].Indexes = append([]SCUMSchemaIndexEvidence(nil), value.Objects[index].Indexes...)
|
||||
value.Objects[index].ForeignKeys = append([]SCUMSchemaForeignKeyEvidence(nil), value.Objects[index].ForeignKeys...)
|
||||
value.Objects[index].SampleFingerprints = append([]string(nil), value.Objects[index].SampleFingerprints...)
|
||||
for idx := range value.Objects[index].Indexes {
|
||||
value.Objects[index].Indexes[idx].ColumnHashes = append([]string(nil), value.Objects[index].Indexes[idx].ColumnHashes...)
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMSchemaProbeRequestPtr(value *SCUMSchemaProbeRequest) *SCUMSchemaProbeRequest {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMSchemaProbeResultPtr(value *SCUMSchemaProbeResult) *SCUMSchemaProbeResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := CopySCUMSchemaProbeResult(*value)
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMLiveDataManifest(value SCUMLiveDataManifest) SCUMLiveDataManifest {
|
||||
value.CapabilityGates = append([]SCUMLiveDataCapabilityGateDeclaration(nil), value.CapabilityGates...)
|
||||
for index := range value.CapabilityGates {
|
||||
value.CapabilityGates[index].RequiredAssetDigests = append([]string(nil), value.CapabilityGates[index].RequiredAssetDigests...)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func sameSCUMBinding(a SCUMBindingIdentity, b SCUMBindingIdentity) bool {
|
||||
return a.ServerInstanceID == b.ServerInstanceID && a.RunBindingID == b.RunBindingID && a.RunEndpointID == b.RunEndpointID && a.PluginID == b.PluginID && a.PluginVersion == b.PluginVersion && a.AdapterVersion == b.AdapterVersion && a.GameVersion == b.GameVersion && a.DatabaseIdentity == b.DatabaseIdentity
|
||||
}
|
||||
|
||||
func containsAllStrings(values []string, required []string) bool {
|
||||
set := map[string]struct{}{}
|
||||
for _, value := range values {
|
||||
set[value] = struct{}{}
|
||||
}
|
||||
for _, value := range required {
|
||||
if _, ok := set[value]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func safeReason(value string, fallback string) string {
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSCUMCapabilityGateDefaultsClosedWithoutProbeEvidence(t *testing.T) {
|
||||
active := scumGateBinding()
|
||||
requirement := SCUMCapabilityRequirement{Capability: SCUMDataCapabilityPlayerRead, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1", AssetDigests: []string{"sha256:query"}}
|
||||
|
||||
gate := EvaluateSCUMCapabilityGate(requirement, SCUMCapabilityEvidence{}, active, true, time.Now())
|
||||
|
||||
if gate.Enabled || gate.State != SCUMCapabilityGateDisabled || gate.ReasonCode != SCUMSafeErrorProbeMissing {
|
||||
t.Fatalf("expected closed gate without evidence, got %#v", gate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMCapabilityGateDefaultsClosedWhenProbeExecutorUnavailable(t *testing.T) {
|
||||
active := scumGateBinding()
|
||||
requirement := SCUMCapabilityRequirement{Capability: SCUMDataCapabilityPlayerRead, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1"}
|
||||
evidence := SCUMCapabilityEvidence{Capability: SCUMDataCapabilityPlayerRead, Status: SCUMCapabilityEvidenceCompatible, Binding: active, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1"}
|
||||
|
||||
gate := EvaluateSCUMCapabilityGate(requirement, evidence, active, false, time.Now())
|
||||
|
||||
if gate.Enabled || gate.ReasonCode != SCUMSafeErrorProbeExecutorAbsent {
|
||||
t.Fatalf("expected executor gate failure, got %#v", gate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMCapabilityGateEnablesOnlyMatchingEvidence(t *testing.T) {
|
||||
now := time.Date(2026, 8, 11, 12, 0, 0, 0, time.UTC)
|
||||
active := scumGateBinding()
|
||||
requirement := SCUMCapabilityRequirement{Capability: SCUMDataCapabilityVehicleRead, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1", AssetDigests: []string{"sha256:vehicle-query"}}
|
||||
evidence := SCUMCapabilityEvidence{Capability: SCUMDataCapabilityVehicleRead, Status: SCUMCapabilityEvidenceCompatible, Binding: active, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1", AssetDigests: []string{"sha256:vehicle-query", "sha256:result-schema"}, ExpiresAt: now.Add(time.Hour)}
|
||||
|
||||
gate := EvaluateSCUMCapabilityGate(requirement, evidence, active, true, now)
|
||||
|
||||
if !gate.Enabled || gate.State != SCUMCapabilityGateEnabled || gate.ReasonCode != SCUMSafeErrorNone {
|
||||
t.Fatalf("expected enabled gate, got %#v", gate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMCapabilityGateRejectsMismatchedCurrentServiceEvidence(t *testing.T) {
|
||||
now := time.Date(2026, 8, 11, 12, 0, 0, 0, time.UTC)
|
||||
active := scumGateBinding()
|
||||
requirement := SCUMCapabilityRequirement{Capability: SCUMDataCapabilityPositionRead, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1", AssetDigests: []string{"sha256:positions"}}
|
||||
evidence := SCUMCapabilityEvidence{Capability: SCUMDataCapabilityPositionRead, Status: SCUMCapabilityEvidenceCompatible, Binding: active, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1", AssetDigests: []string{"sha256:positions"}, ExpiresAt: now.Add(time.Hour)}
|
||||
|
||||
changedBinding := evidence
|
||||
changedBinding.Binding.DatabaseIdentity = "db-other"
|
||||
if gate := EvaluateSCUMCapabilityGate(requirement, changedBinding, active, true, now); gate.Enabled || gate.ReasonCode != SCUMSafeErrorBindingMismatch {
|
||||
t.Fatalf("expected binding mismatch, got %#v", gate)
|
||||
}
|
||||
|
||||
changedFingerprint := evidence
|
||||
changedFingerprint.SchemaFingerprint = "schema-other"
|
||||
if gate := EvaluateSCUMCapabilityGate(requirement, changedFingerprint, active, true, now); gate.Enabled || gate.ReasonCode != SCUMSafeErrorFingerprintMismatch {
|
||||
t.Fatalf("expected fingerprint mismatch, got %#v", gate)
|
||||
}
|
||||
|
||||
changedDigest := evidence
|
||||
changedDigest.AssetDigests = []string{"sha256:different"}
|
||||
if gate := EvaluateSCUMCapabilityGate(requirement, changedDigest, active, true, now); gate.Enabled || gate.ReasonCode != SCUMSafeErrorDigestMismatch {
|
||||
t.Fatalf("expected digest mismatch, got %#v", gate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultSCUMSchemaProbeBoundsAreBoundedAndDiagnosticOnly(t *testing.T) {
|
||||
bounds := DefaultSCUMSchemaProbeBounds()
|
||||
if bounds.MaxObjects <= 0 || bounds.MaxSampleRows > 3 || bounds.TimeoutMS > 5000 || bounds.MaxResultBytes > 512*1024 {
|
||||
t.Fatalf("unexpected unsafe default probe bounds: %#v", bounds)
|
||||
}
|
||||
}
|
||||
|
||||
func scumGateBinding() SCUMBindingIdentity {
|
||||
return SCUMBindingIdentity{ServerInstanceID: "server-1", RunBindingID: "binding-1", RunEndpointID: "run-1", PluginID: "game.scum", PluginVersion: "0.1.6", AdapterVersion: "adapter-1", GameVersion: "scum-1", DatabaseIdentity: "db-current"}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type SCUMProjectionSubject string
|
||||
|
||||
const (
|
||||
SCUMProjectionSubjectPlayer SCUMProjectionSubject = "player"
|
||||
SCUMProjectionSubjectLiveState SCUMProjectionSubject = "player-live-state"
|
||||
SCUMProjectionSubjectSquad SCUMProjectionSubject = "squad"
|
||||
SCUMProjectionSubjectMember SCUMProjectionSubject = "squad-member"
|
||||
SCUMProjectionSubjectVehicle SCUMProjectionSubject = "vehicle"
|
||||
SCUMProjectionSubjectFlag SCUMProjectionSubject = "flag"
|
||||
SCUMProjectionSubjectPosition SCUMProjectionSubject = "position"
|
||||
)
|
||||
|
||||
type SCUMProjectionFilter struct {
|
||||
ServerInstanceID string
|
||||
GamePlayerID string
|
||||
GamePlayerRecordID string
|
||||
UserProfileID string
|
||||
SteamID string
|
||||
SquadID string
|
||||
VehicleID string
|
||||
FlagID string
|
||||
SubjectType SCUMProjectionSubject
|
||||
QueryKey string
|
||||
Freshness SCUMProjectionFreshness
|
||||
Search string
|
||||
Limit int
|
||||
}
|
||||
|
||||
type SCUMPlayerLiveState struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
GamePlayerRecordID string
|
||||
GamePlayerID string
|
||||
UserProfileID string
|
||||
SteamID string
|
||||
DisplayName string
|
||||
SquadID string
|
||||
SquadName string
|
||||
Online bool
|
||||
FamePoints float64
|
||||
NormalBalance float64
|
||||
GoldBalance float64
|
||||
LastLoginAt time.Time
|
||||
LastLogoutAt time.Time
|
||||
LastSaveTime time.Time
|
||||
Position SCUMCurrentPosition
|
||||
UnknownFields map[string]any
|
||||
Freshness SCUMProjectionFreshnessState
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMSquad struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
SquadID string
|
||||
Name string
|
||||
LeaderProfileID string
|
||||
LeaderPlayerID string
|
||||
MemberCount int
|
||||
Score float64
|
||||
UnknownFields map[string]any
|
||||
Freshness SCUMProjectionFreshnessState
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMSquadMember struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
SquadID string
|
||||
UserProfileID string
|
||||
GamePlayerRecordID string
|
||||
GamePlayerID string
|
||||
SteamID string
|
||||
DisplayName string
|
||||
Rank string
|
||||
IsLeader bool
|
||||
JoinedAt time.Time
|
||||
UnknownFields map[string]any
|
||||
Freshness SCUMProjectionFreshnessState
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMVehicle struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
VehicleID string
|
||||
EntityID string
|
||||
ClassName string
|
||||
Label string
|
||||
OwnerProfileID string
|
||||
OwnerPlayerID string
|
||||
SquadID string
|
||||
Position SCUMCurrentPosition
|
||||
UnknownFields map[string]any
|
||||
Freshness SCUMProjectionFreshnessState
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMFlag struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
FlagID string
|
||||
EntityID string
|
||||
OwnerProfileID string
|
||||
OwnerPlayerID string
|
||||
OwnerSquadID string
|
||||
OwnerSquadName string
|
||||
OwnershipConfidence string
|
||||
Position SCUMCurrentPosition
|
||||
UnknownFields map[string]any
|
||||
Freshness SCUMProjectionFreshnessState
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMCurrentPosition struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
SubjectType SCUMProjectionSubject
|
||||
SubjectID string
|
||||
GamePlayerRecordID string
|
||||
GamePlayerID string
|
||||
VehicleID string
|
||||
EntityID string
|
||||
MapID string
|
||||
MapVersion string
|
||||
X float64
|
||||
Y float64
|
||||
Z float64
|
||||
HasCoordinates bool
|
||||
LastSaveTime time.Time
|
||||
Freshness SCUMProjectionFreshnessState
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func SCUMProjectionStateUnknown() SCUMProjectionFreshnessState {
|
||||
return SCUMProjectionFreshnessState{Status: SCUMProjectionUnknown}
|
||||
}
|
||||
|
||||
func CopySCUMPlayerLiveState(value SCUMPlayerLiveState) SCUMPlayerLiveState {
|
||||
value.Position = CopySCUMCurrentPosition(value.Position)
|
||||
value.UnknownFields = CopyGameClientBridgePayload(value.UnknownFields)
|
||||
value.Freshness = CopySCUMProjectionFreshnessState(value.Freshness)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMSquad(value SCUMSquad) SCUMSquad {
|
||||
value.UnknownFields = CopyGameClientBridgePayload(value.UnknownFields)
|
||||
value.Freshness = CopySCUMProjectionFreshnessState(value.Freshness)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMSquadMember(value SCUMSquadMember) SCUMSquadMember {
|
||||
value.UnknownFields = CopyGameClientBridgePayload(value.UnknownFields)
|
||||
value.Freshness = CopySCUMProjectionFreshnessState(value.Freshness)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMVehicle(value SCUMVehicle) SCUMVehicle {
|
||||
value.Position = CopySCUMCurrentPosition(value.Position)
|
||||
value.UnknownFields = CopyGameClientBridgePayload(value.UnknownFields)
|
||||
value.Freshness = CopySCUMProjectionFreshnessState(value.Freshness)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMFlag(value SCUMFlag) SCUMFlag {
|
||||
value.Position = CopySCUMCurrentPosition(value.Position)
|
||||
value.UnknownFields = CopyGameClientBridgePayload(value.UnknownFields)
|
||||
value.Freshness = CopySCUMProjectionFreshnessState(value.Freshness)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMCurrentPosition(value SCUMCurrentPosition) SCUMCurrentPosition {
|
||||
value.Freshness = CopySCUMProjectionFreshnessState(value.Freshness)
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type SCUMObservationStatus string
|
||||
|
||||
const (
|
||||
SCUMObservationAccepted SCUMObservationStatus = "accepted"
|
||||
SCUMObservationStale SCUMObservationStatus = "stale"
|
||||
SCUMObservationFailed SCUMObservationStatus = "failed"
|
||||
)
|
||||
|
||||
type SCUMProjectionFreshness string
|
||||
|
||||
const (
|
||||
SCUMProjectionFresh SCUMProjectionFreshness = "fresh"
|
||||
SCUMProjectionStale SCUMProjectionFreshness = "stale"
|
||||
SCUMProjectionUnknown SCUMProjectionFreshness = "unknown"
|
||||
)
|
||||
|
||||
type SCUMWorkflowStatus string
|
||||
|
||||
const (
|
||||
SCUMWorkflowDraft SCUMWorkflowStatus = "draft"
|
||||
SCUMWorkflowQueued SCUMWorkflowStatus = "queued"
|
||||
SCUMWorkflowRunning SCUMWorkflowStatus = "running"
|
||||
SCUMWorkflowWaiting SCUMWorkflowStatus = "waiting"
|
||||
SCUMWorkflowBlocked SCUMWorkflowStatus = "blocked"
|
||||
SCUMWorkflowConfirming SCUMWorkflowStatus = "confirming"
|
||||
SCUMWorkflowConfirmed SCUMWorkflowStatus = "confirmed"
|
||||
SCUMWorkflowFailed SCUMWorkflowStatus = "failed"
|
||||
SCUMWorkflowUnknown SCUMWorkflowStatus = "unknown"
|
||||
SCUMWorkflowCancelled SCUMWorkflowStatus = "cancelled"
|
||||
)
|
||||
|
||||
type SCUMWorkflowStepStatus string
|
||||
|
||||
const (
|
||||
SCUMWorkflowStepQueued SCUMWorkflowStepStatus = "queued"
|
||||
SCUMWorkflowStepRunning SCUMWorkflowStepStatus = "running"
|
||||
SCUMWorkflowStepWaiting SCUMWorkflowStepStatus = "waiting"
|
||||
SCUMWorkflowStepBlocked SCUMWorkflowStepStatus = "blocked"
|
||||
SCUMWorkflowStepConfirming SCUMWorkflowStepStatus = "confirming"
|
||||
SCUMWorkflowStepConfirmed SCUMWorkflowStepStatus = "confirmed"
|
||||
SCUMWorkflowStepFailed SCUMWorkflowStepStatus = "failed"
|
||||
SCUMWorkflowStepUnknown SCUMWorkflowStepStatus = "unknown"
|
||||
SCUMWorkflowStepCancelled SCUMWorkflowStepStatus = "cancelled"
|
||||
)
|
||||
|
||||
type SCUMSafeSummary struct {
|
||||
Title string
|
||||
Message string
|
||||
Details map[string]string
|
||||
}
|
||||
|
||||
type SCUMDataObservation struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
Source string
|
||||
QueryKey string
|
||||
SubjectType string
|
||||
SubjectID string
|
||||
Sequence uint64
|
||||
Checksum string
|
||||
Status SCUMObservationStatus
|
||||
ErrorCode string
|
||||
SafeSummary SCUMSafeSummary
|
||||
ObservedAt time.Time
|
||||
ReceivedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMObservationResult struct {
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
Source string
|
||||
QueryKey string
|
||||
Sequence uint64
|
||||
Checksum string
|
||||
Status SCUMObservationStatus
|
||||
ErrorCode string
|
||||
SafeSummary SCUMSafeSummary
|
||||
ObservedAt time.Time
|
||||
ReceivedAt time.Time
|
||||
Rows []map[string]any
|
||||
}
|
||||
|
||||
type SCUMProjectionFreshnessState struct {
|
||||
Status SCUMProjectionFreshness
|
||||
ObservationID string
|
||||
Source string
|
||||
QueryKey string
|
||||
Sequence uint64
|
||||
Checksum string
|
||||
StaleReason string
|
||||
ObservedAt time.Time
|
||||
ReceivedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMMutationGuard struct {
|
||||
FieldKey string
|
||||
Before any
|
||||
After any
|
||||
MaxRowsAffected int
|
||||
SafetyWindow string
|
||||
BackupRef string
|
||||
RequiresOfflinePlayer bool
|
||||
RequiresMaintenance bool
|
||||
RequiresBackup bool
|
||||
}
|
||||
|
||||
type SCUMOperationConfirmation struct {
|
||||
Status string
|
||||
ObservationID string
|
||||
ConfirmedFields map[string]any
|
||||
AffectedRows int
|
||||
MutationChecksum string
|
||||
Checksum string
|
||||
ObservedAt time.Time
|
||||
SafeSummary SCUMSafeSummary
|
||||
}
|
||||
|
||||
type SCUMOperationRequest struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
TemplateKey string
|
||||
PlayerID string
|
||||
RequesterID string
|
||||
ApproverID string
|
||||
ApprovalLevel GameClientBridgeApprovalLevel
|
||||
Payload map[string]any
|
||||
Guard SCUMMutationGuard
|
||||
Confirmation SCUMOperationConfirmation
|
||||
Status SCUMWorkflowStepStatus
|
||||
Reason string
|
||||
IdempotencyKey string
|
||||
RunJobID string
|
||||
SafeSummary SCUMSafeSummary
|
||||
AuditReferences []string
|
||||
CreatedAt time.Time
|
||||
ApprovedAt time.Time
|
||||
CompletedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMOperationRequestFilter struct {
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
TemplateKey string
|
||||
PlayerID string
|
||||
RequesterID string
|
||||
Status SCUMWorkflowStepStatus
|
||||
IdempotencyKey string
|
||||
Limit int
|
||||
}
|
||||
|
||||
type SCUMWorkflowInstanceFilter struct {
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
TemplateKey string
|
||||
RequestedBy string
|
||||
Status SCUMWorkflowStatus
|
||||
IdempotencyKey string
|
||||
Limit int
|
||||
}
|
||||
|
||||
type SCUMWorkflowStepFilter struct {
|
||||
WorkflowID string
|
||||
ServerInstanceID string
|
||||
StepKey string
|
||||
Status SCUMWorkflowStepStatus
|
||||
MutatesState *bool
|
||||
Limit int
|
||||
}
|
||||
|
||||
type SCUMWorkflowInstance struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
TemplateKey string
|
||||
RequestedBy string
|
||||
IdempotencyKey string
|
||||
Status SCUMWorkflowStatus
|
||||
CurrentStepKey string
|
||||
Input map[string]any
|
||||
SafeSummary SCUMSafeSummary
|
||||
BlockerReason string
|
||||
AuditReferences []string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
CompletedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMWorkflowStep struct {
|
||||
ID string
|
||||
WorkflowID string
|
||||
ServerInstanceID string
|
||||
StepKey string
|
||||
DependsOn []string
|
||||
Status SCUMWorkflowStepStatus
|
||||
OperationKey string
|
||||
QueryTemplateKey string
|
||||
Capability string
|
||||
TargetKey string
|
||||
JobID string
|
||||
Attempt int
|
||||
MaxAttempts int
|
||||
MutatesState bool
|
||||
Confirmation SCUMOperationConfirmation
|
||||
SafeSummary SCUMSafeSummary
|
||||
BlockerReason string
|
||||
AuditReferences []string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
CompletedAt time.Time
|
||||
}
|
||||
|
||||
func CopySCUMSafeSummary(value SCUMSafeSummary) SCUMSafeSummary {
|
||||
value.Details = CopyStringMap(value.Details)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMDataObservation(value SCUMDataObservation) SCUMDataObservation {
|
||||
value.SafeSummary = CopySCUMSafeSummary(value.SafeSummary)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMObservationResult(value SCUMObservationResult) SCUMObservationResult {
|
||||
value.SafeSummary = CopySCUMSafeSummary(value.SafeSummary)
|
||||
value.Rows = CopyGameClientBridgeRows(value.Rows)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopyGameClientBridgeRows(values []map[string]any) []map[string]any {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]map[string]any, len(values))
|
||||
for index, row := range values {
|
||||
out[index] = CopyGameClientBridgePayload(row)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func CopySCUMProjectionFreshnessState(value SCUMProjectionFreshnessState) SCUMProjectionFreshnessState {
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMMutationGuard(value SCUMMutationGuard) SCUMMutationGuard {
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMOperationConfirmation(value SCUMOperationConfirmation) SCUMOperationConfirmation {
|
||||
value.ConfirmedFields = CopyGameClientBridgePayload(value.ConfirmedFields)
|
||||
value.SafeSummary = CopySCUMSafeSummary(value.SafeSummary)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMOperationRequest(value SCUMOperationRequest) SCUMOperationRequest {
|
||||
value.Payload = CopyGameClientBridgePayload(value.Payload)
|
||||
value.Guard = CopySCUMMutationGuard(value.Guard)
|
||||
value.Confirmation = CopySCUMOperationConfirmation(value.Confirmation)
|
||||
value.SafeSummary = CopySCUMSafeSummary(value.SafeSummary)
|
||||
value.AuditReferences = CopyStringSlice(value.AuditReferences)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMWorkflowInstance(value SCUMWorkflowInstance) SCUMWorkflowInstance {
|
||||
value.Input = CopyGameClientBridgePayload(value.Input)
|
||||
value.SafeSummary = CopySCUMSafeSummary(value.SafeSummary)
|
||||
value.AuditReferences = CopyStringSlice(value.AuditReferences)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMWorkflowStep(value SCUMWorkflowStep) SCUMWorkflowStep {
|
||||
value.DependsOn = CopyStringSlice(value.DependsOn)
|
||||
value.Confirmation = CopySCUMOperationConfirmation(value.Confirmation)
|
||||
value.SafeSummary = CopySCUMSafeSummary(value.SafeSummary)
|
||||
value.AuditReferences = CopyStringSlice(value.AuditReferences)
|
||||
return value
|
||||
}
|
||||
+20
-92
@@ -93,91 +93,24 @@ type RunJobResultRequest struct {
|
||||
}
|
||||
|
||||
type RunJobExecutionInputBody struct {
|
||||
WorkspaceScope string `json:"workspaceScope,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ExpectedVersion int `json:"expectedVersion,omitempty"`
|
||||
ExpectedChecksum string `json:"expectedChecksum,omitempty"`
|
||||
MaxReadBytes int `json:"maxReadBytes,omitempty"`
|
||||
RemoteAdapterKey string `json:"remoteAdapterKey,omitempty"`
|
||||
RemoteAdapterKind string `json:"remoteAdapterKind,omitempty"`
|
||||
TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
|
||||
PluginID string `json:"pluginId,omitempty"`
|
||||
LifecycleOperation string `json:"lifecycleOperation,omitempty"`
|
||||
TargetVersion string `json:"targetVersion,omitempty"`
|
||||
Inputs map[string]string `json:"inputs,omitempty"`
|
||||
LogSource *RuntimeLogSourceBody `json:"logSource,omitempty"`
|
||||
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
|
||||
DLLExtensions []RuntimeDLLExtensionPlanBody `json:"dllExtensions,omitempty"`
|
||||
SourceRCON *RuntimeSourceRCONPlanBody `json:"sourceRcon,omitempty"`
|
||||
Deployment *ServerDeploymentExecutionBody `json:"deployment,omitempty"`
|
||||
ServerDeploymentPlan *ServerDeploymentPlanBody `json:"serverDeploymentPlan,omitempty"`
|
||||
SQLiteSchemaProbe *RunSQLiteSchemaProbeRequestBody `json:"sqliteSchemaProbe,omitempty"`
|
||||
SQLiteTemplate *RunSQLiteTemplateRequestBody `json:"sqliteTemplate,omitempty"`
|
||||
RCONTemplate *RunTypedRCONTemplateRequestBody `json:"rconTemplate,omitempty"`
|
||||
GuardedMutation *RunGuardedMutationRequestBody `json:"guardedMutation,omitempty"`
|
||||
}
|
||||
|
||||
type RunSQLiteSchemaProbeRequestBody struct {
|
||||
RequestID string `json:"requestId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Limits SCUMSchemaProbeBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type RunSQLiteTemplateRequestBody struct {
|
||||
RequestID string `json:"requestId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Capability string `json:"capability"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
RequiredSchemaFingerprint string `json:"requiredSchemaFingerprint"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
ParameterDigest string `json:"parameterDigest"`
|
||||
Parameters map[string]any `json:"parameters,omitempty"`
|
||||
Limits SCUMSQLiteTemplateBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type RunTypedRCONTemplateRequestBody struct {
|
||||
RequestID string `json:"requestId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Capability string `json:"capability"`
|
||||
TransportKey string `json:"transportKey"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
RequiredSchemaFingerprint string `json:"requiredSchemaFingerprint,omitempty"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
PayloadDigest string `json:"payloadDigest"`
|
||||
ConfirmationDigest string `json:"confirmationDigest"`
|
||||
TargetIdentityDigest string `json:"targetIdentityDigest"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
ReviewReason string `json:"reviewReason"`
|
||||
Limits SCUMTypedRCONTemplateBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type RunGuardedMutationRequestBody struct {
|
||||
RequestID string `json:"requestId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Capability string `json:"capability"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
RequiredSchemaFingerprint string `json:"requiredSchemaFingerprint"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
TargetIdentityDigest string `json:"targetIdentityDigest"`
|
||||
ExpectedRowDigest string `json:"expectedRowDigest"`
|
||||
ExpectedValueDigest string `json:"expectedValueDigest"`
|
||||
ExpectedXMLDigest string `json:"expectedXmlDigest"`
|
||||
PatchDigest string `json:"patchDigest"`
|
||||
BackupEvidenceDigest string `json:"backupEvidenceDigest"`
|
||||
OfflineEvidenceDigest string `json:"offlineEvidenceDigest"`
|
||||
DangerConfirmationDigest string `json:"dangerConfirmationDigest"`
|
||||
ReadbackExpectationDigest string `json:"readbackExpectationDigest"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
ReviewReason string `json:"reviewReason"`
|
||||
Limits SCUMGuardedMutationBoundsDTO `json:"limits"`
|
||||
WorkspaceScope string `json:"workspaceScope,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ExpectedVersion int `json:"expectedVersion,omitempty"`
|
||||
ExpectedChecksum string `json:"expectedChecksum,omitempty"`
|
||||
MaxReadBytes int `json:"maxReadBytes,omitempty"`
|
||||
RemoteAdapterKey string `json:"remoteAdapterKey,omitempty"`
|
||||
RemoteAdapterKind string `json:"remoteAdapterKind,omitempty"`
|
||||
TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
|
||||
PluginID string `json:"pluginId,omitempty"`
|
||||
LifecycleOperation string `json:"lifecycleOperation,omitempty"`
|
||||
TargetVersion string `json:"targetVersion,omitempty"`
|
||||
Inputs map[string]string `json:"inputs,omitempty"`
|
||||
LogSource *RuntimeLogSourceBody `json:"logSource,omitempty"`
|
||||
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
|
||||
DLLExtensions []RuntimeDLLExtensionPlanBody `json:"dllExtensions,omitempty"`
|
||||
SourceRCON *RuntimeSourceRCONPlanBody `json:"sourceRcon,omitempty"`
|
||||
Deployment *ServerDeploymentExecutionBody `json:"deployment,omitempty"`
|
||||
ServerDeploymentPlan *ServerDeploymentPlanBody `json:"serverDeploymentPlan,omitempty"`
|
||||
}
|
||||
|
||||
type ServerDeploymentPlanBody struct {
|
||||
@@ -256,11 +189,6 @@ type RunJobExecutionResultBody struct {
|
||||
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
||||
AuditSummary string `json:"auditSummary,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
SQLiteSchemaProbe *SCUMSchemaProbeResultDTO `json:"sqliteSchemaProbe,omitempty"`
|
||||
SQLiteTemplate *SCUMSQLiteTemplateResultDTO `json:"sqliteTemplate,omitempty"`
|
||||
RCONTemplate *SCUMTypedRCONTemplateResultDTO `json:"rconTemplate,omitempty"`
|
||||
GuardedMutation *SCUMGuardedMutationResultDTO `json:"guardedMutation,omitempty"`
|
||||
ParsedLogBatch *SCUMParsedLogBatchResultDTO `json:"parsedLogBatch,omitempty"`
|
||||
ServerDeploymentEvidence *ServerDeploymentEvidenceBody `json:"serverDeploymentEvidence,omitempty"`
|
||||
DeploymentReceipt *ServerDeploymentExecutionReceiptBody `json:"deploymentReceipt,omitempty"`
|
||||
}
|
||||
@@ -520,7 +448,7 @@ func (request RunJobResultRequest) ToDomain() domain.RunJobResult {
|
||||
Message: request.Message,
|
||||
ErrorCode: request.ErrorCode,
|
||||
Retryable: request.Retryable,
|
||||
ExecutionResult: domain.JobExecutionResult{Kind: request.ExecutionResult.Kind, ProcessState: request.ExecutionResult.ProcessState, ExitClassification: request.ExecutionResult.ExitClassification, ExitCode: request.ExecutionResult.ExitCode, Version: request.ExecutionResult.Version, Checksum: request.ExecutionResult.Checksum, SizeBytes: request.ExecutionResult.SizeBytes, AuditSummary: request.ExecutionResult.AuditSummary, Content: request.ExecutionResult.Content, SQLiteSchemaProbe: SCUMSchemaProbeResultPtrToDomain(request.ExecutionResult.SQLiteSchemaProbe), SQLiteTemplate: SCUMSQLiteTemplateResultPtrToDomain(request.ExecutionResult.SQLiteTemplate), RCONTemplate: SCUMTypedRCONTemplateResultPtrToDomain(request.ExecutionResult.RCONTemplate), GuardedMutation: SCUMGuardedMutationResultPtrToDomain(request.ExecutionResult.GuardedMutation), ParsedLogBatch: SCUMParsedLogBatchResultPtrToDomain(request.ExecutionResult.ParsedLogBatch), ServerDeploymentEvidence: serverDeploymentEvidenceToDomain(request.ExecutionResult.ServerDeploymentEvidence), DeploymentReceipt: deploymentReceiptToDomain(request.ExecutionResult.DeploymentReceipt)},
|
||||
ExecutionResult: domain.JobExecutionResult{Kind: request.ExecutionResult.Kind, ProcessState: request.ExecutionResult.ProcessState, ExitClassification: request.ExecutionResult.ExitClassification, ExitCode: request.ExecutionResult.ExitCode, Version: request.ExecutionResult.Version, Checksum: request.ExecutionResult.Checksum, SizeBytes: request.ExecutionResult.SizeBytes, AuditSummary: request.ExecutionResult.AuditSummary, Content: request.ExecutionResult.Content, ServerDeploymentEvidence: serverDeploymentEvidenceToDomain(request.ExecutionResult.ServerDeploymentEvidence), DeploymentReceipt: deploymentReceiptToDomain(request.ExecutionResult.DeploymentReceipt)},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -735,7 +663,7 @@ func RunJobAssignmentFromDomain(assignment domain.RunJobAssignment) RunJobAssign
|
||||
State: assignment.State,
|
||||
Progress: progressReportFromDomain(assignment.Progress),
|
||||
ResultRef: assignment.ResultRef,
|
||||
ExecutionInput: RunJobExecutionInputBody{WorkspaceScope: assignment.ExecutionInput.WorkspaceScope, Content: assignment.ExecutionInput.Content, ExpectedVersion: assignment.ExecutionInput.ExpectedVersion, ExpectedChecksum: assignment.ExecutionInput.ExpectedChecksum, MaxReadBytes: assignment.ExecutionInput.MaxReadBytes, RemoteAdapterKey: assignment.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: assignment.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: assignment.ExecutionInput.TimeoutSeconds, PluginID: assignment.ExecutionInput.PluginID, LifecycleOperation: assignment.ExecutionInput.LifecycleOperation, TargetVersion: assignment.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(assignment.ExecutionInput.Inputs), LogSource: runtimeLogSourceFromDomain(assignment.ExecutionInput.LogSource), LogSources: runtimeLogSourcesFromDomain(assignment.ExecutionInput.LogSources), DLLExtensions: dllExtensionPlansFromDomain(assignment.ExecutionInput.DLLExtensions), SourceRCON: runtimeSourceRCONPlanFromDomain(assignment.ExecutionInput.SourceRCON), Deployment: deploymentExecutionFromDomain(assignment.ExecutionInput.Deployment), ServerDeploymentPlan: serverDeploymentPlanFromDomain(assignment.ExecutionInput.ServerDeploymentPlan), SQLiteSchemaProbe: runSQLiteSchemaProbeRequestPtrFromDomain(assignment.ExecutionInput.SQLiteSchemaProbe), SQLiteTemplate: runSQLiteTemplateRequestPtrFromDomain(assignment.ExecutionInput.SQLiteTemplate), RCONTemplate: runTypedRCONTemplateRequestPtrFromDomain(assignment.ExecutionInput.RCONTemplate), GuardedMutation: runGuardedMutationRequestPtrFromDomain(assignment.ExecutionInput.GuardedMutation)},
|
||||
ExecutionInput: RunJobExecutionInputBody{WorkspaceScope: assignment.ExecutionInput.WorkspaceScope, Content: assignment.ExecutionInput.Content, ExpectedVersion: assignment.ExecutionInput.ExpectedVersion, ExpectedChecksum: assignment.ExecutionInput.ExpectedChecksum, MaxReadBytes: assignment.ExecutionInput.MaxReadBytes, RemoteAdapterKey: assignment.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: assignment.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: assignment.ExecutionInput.TimeoutSeconds, PluginID: assignment.ExecutionInput.PluginID, LifecycleOperation: assignment.ExecutionInput.LifecycleOperation, TargetVersion: assignment.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(assignment.ExecutionInput.Inputs), LogSource: runtimeLogSourceFromDomain(assignment.ExecutionInput.LogSource), LogSources: runtimeLogSourcesFromDomain(assignment.ExecutionInput.LogSources), DLLExtensions: dllExtensionPlansFromDomain(assignment.ExecutionInput.DLLExtensions), SourceRCON: runtimeSourceRCONPlanFromDomain(assignment.ExecutionInput.SourceRCON), Deployment: deploymentExecutionFromDomain(assignment.ExecutionInput.Deployment), ServerDeploymentPlan: serverDeploymentPlanFromDomain(assignment.ExecutionInput.ServerDeploymentPlan)},
|
||||
LeaseToken: assignment.LeaseToken,
|
||||
Attempt: assignment.Attempt,
|
||||
FencingToken: assignment.FencingToken,
|
||||
|
||||
@@ -399,28 +399,6 @@ type GameMapTrajectoryDeclarationBody struct {
|
||||
RetentionSeconds int `json:"retentionSeconds"`
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeDeclarationBody struct {
|
||||
Capability string `json:"capability"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
Bounds SCUMSchemaProbeBoundsDTO `json:"bounds"`
|
||||
}
|
||||
|
||||
type SCUMLiveDataCapabilityGateBody struct {
|
||||
Capability string `json:"capability"`
|
||||
Gate string `json:"gate"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
RequiredSchemaFingerprint string `json:"requiredSchemaFingerprint,omitempty"`
|
||||
RequiredAssetDigests []string `json:"requiredAssetDigests,omitempty"`
|
||||
EvidenceStatus string `json:"evidenceStatus"`
|
||||
SafeReason string `json:"safeReason"`
|
||||
}
|
||||
|
||||
type SCUMLiveDataManifestBody struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
Probe SCUMSchemaProbeDeclarationBody `json:"probe"`
|
||||
CapabilityGates []SCUMLiveDataCapabilityGateBody `json:"capabilityGates"`
|
||||
}
|
||||
|
||||
type GamePluginManifestBody struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -441,7 +419,6 @@ type GamePluginManifestBody struct {
|
||||
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
|
||||
RuntimeProfiles GamePluginRuntimeProfilesBody `json:"runtimeProfiles,omitempty"`
|
||||
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
|
||||
SCUMLiveData SCUMLiveDataManifestBody `json:"scumLiveData,omitempty"`
|
||||
MapTrajectories *GameMapTrajectoryDeclarationBody `json:"mapTrajectories,omitempty"`
|
||||
}
|
||||
|
||||
@@ -481,7 +458,6 @@ type GamePluginCreateRequest struct {
|
||||
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
|
||||
RuntimeProfiles GamePluginRuntimeProfilesBody `json:"runtimeProfiles,omitempty"`
|
||||
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
|
||||
SCUMLiveData SCUMLiveDataManifestBody `json:"scumLiveData,omitempty"`
|
||||
MapTrajectories *GameMapTrajectoryDeclarationBody `json:"mapTrajectories,omitempty"`
|
||||
ValidationViolations []string `json:"validationViolations,omitempty"`
|
||||
}
|
||||
@@ -510,7 +486,6 @@ type GamePluginResponse struct {
|
||||
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
|
||||
RuntimeProfiles GamePluginRuntimeProfilesResponseBody `json:"runtimeProfiles,omitempty"`
|
||||
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
|
||||
SCUMLiveData *SCUMLiveDataManifestBody `json:"scumLiveData,omitempty"`
|
||||
MapTrajectories *GameMapTrajectoryDeclarationBody `json:"mapTrajectories,omitempty"`
|
||||
ValidationViolations []string `json:"validationViolations,omitempty"`
|
||||
Status domain.GamePluginStatus `json:"status"`
|
||||
@@ -884,7 +859,6 @@ type JobExecutionResultResponse struct {
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
||||
AuditSummary string `json:"auditSummary,omitempty"`
|
||||
SQLiteSchemaProbe *SCUMSchemaProbeResultDTO `json:"sqliteSchemaProbe,omitempty"`
|
||||
ServerDeploymentEvidence *ServerDeploymentEvidenceBody `json:"serverDeploymentEvidence,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1108,20 +1082,11 @@ func (request GamePluginManifestRegistrationRequest) ToDomain() domain.GamePlugi
|
||||
RemoteAccess: request.Manifest.RemoteAccess.ToDomain(),
|
||||
RuntimeProfiles: request.Manifest.RuntimeProfiles.ToDomain(),
|
||||
GameClientBridge: request.Manifest.GameClientBridge.ToDomain(),
|
||||
SCUMLiveData: request.Manifest.SCUMLiveData.ToDomain(),
|
||||
MapTrajectories: mapTrajectoryDeclarationToDomain(request.Manifest.MapTrajectories),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (body SCUMLiveDataManifestBody) ToDomain() domain.SCUMLiveDataManifest {
|
||||
gates := make([]domain.SCUMLiveDataCapabilityGateDeclaration, len(body.CapabilityGates))
|
||||
for index, gate := range body.CapabilityGates {
|
||||
gates[index] = domain.SCUMLiveDataCapabilityGateDeclaration{Capability: domain.SCUMDataCapability(gate.Capability), Gate: domain.SCUMCapabilityGateState(gate.Gate), AdapterVersion: gate.AdapterVersion, RequiredSchemaFingerprint: gate.RequiredSchemaFingerprint, RequiredAssetDigests: domain.CopyStringSlice(gate.RequiredAssetDigests), EvidenceStatus: domain.SCUMCapabilityEvidenceStatus(gate.EvidenceStatus), SafeReason: gate.SafeReason}
|
||||
}
|
||||
return domain.SCUMLiveDataManifest{SchemaVersion: body.SchemaVersion, Probe: domain.SCUMSchemaProbeDeclaration{Capability: body.Probe.Capability, TargetKey: body.Probe.TargetKey, Bounds: scumProbeBoundsToDomain(body.Probe.Bounds)}, CapabilityGates: gates}
|
||||
}
|
||||
|
||||
func pluginAssetFilesToDomain(files []PluginAssetFileBody) []domain.PluginAssetFile {
|
||||
if files == nil {
|
||||
return nil
|
||||
@@ -1303,7 +1268,6 @@ func (request GamePluginCreateRequest) ToDomain() domain.GamePlugin {
|
||||
RemoteAccess: request.RemoteAccess.ToDomain(),
|
||||
RuntimeProfiles: request.RuntimeProfiles.ToDomain(),
|
||||
GameClientBridge: request.GameClientBridge.ToDomain(),
|
||||
SCUMLiveData: request.SCUMLiveData.ToDomain(),
|
||||
MapTrajectories: mapTrajectoryDeclarationToDomain(request.MapTrajectories),
|
||||
ValidationViolations: domain.CopyStringSlice(request.ValidationViolations),
|
||||
}
|
||||
@@ -1557,7 +1521,6 @@ func GamePluginFromDomain(plugin domain.GamePlugin) GamePluginResponse {
|
||||
RemoteAccess: remoteAccessFromDomain(plugin.RemoteAccess),
|
||||
RuntimeProfiles: runtimeProfilesFromDomain(plugin.RuntimeProfiles),
|
||||
GameClientBridge: gameClientBridgeManifestFromDomain(plugin.GameClientBridge),
|
||||
SCUMLiveData: scumLiveDataManifestPtrFromDomain(plugin.SCUMLiveData),
|
||||
MapTrajectories: mapTrajectoryDeclarationFromDomain(plugin.MapTrajectories),
|
||||
ValidationViolations: plugin.ValidationViolations,
|
||||
Status: plugin.Status,
|
||||
@@ -1662,23 +1625,6 @@ func productionLifecycleFromDomain(lifecycle domain.GamePluginProductionLifecycl
|
||||
return GamePluginProductionLifecycleBody{Operations: lifecycle.Operations, DependencyPolicy: lifecycle.DependencyPolicy, ApprovalRequired: lifecycle.ApprovalRequired}
|
||||
}
|
||||
|
||||
func scumLiveDataManifestFromDomain(value domain.SCUMLiveDataManifest) SCUMLiveDataManifestBody {
|
||||
value = domain.CopySCUMLiveDataManifest(value)
|
||||
gates := make([]SCUMLiveDataCapabilityGateBody, len(value.CapabilityGates))
|
||||
for index, gate := range value.CapabilityGates {
|
||||
gates[index] = SCUMLiveDataCapabilityGateBody{Capability: string(gate.Capability), Gate: string(gate.Gate), AdapterVersion: gate.AdapterVersion, RequiredSchemaFingerprint: gate.RequiredSchemaFingerprint, RequiredAssetDigests: domain.CopyStringSlice(gate.RequiredAssetDigests), EvidenceStatus: string(gate.EvidenceStatus), SafeReason: gate.SafeReason}
|
||||
}
|
||||
return SCUMLiveDataManifestBody{SchemaVersion: value.SchemaVersion, Probe: SCUMSchemaProbeDeclarationBody{Capability: value.Probe.Capability, TargetKey: value.Probe.TargetKey, Bounds: scumProbeBoundsFromDomain(value.Probe.Bounds)}, CapabilityGates: gates}
|
||||
}
|
||||
|
||||
func scumLiveDataManifestPtrFromDomain(value domain.SCUMLiveDataManifest) *SCUMLiveDataManifestBody {
|
||||
if value.SchemaVersion == "" && value.Probe.Capability == "" && len(value.CapabilityGates) == 0 {
|
||||
return nil
|
||||
}
|
||||
body := scumLiveDataManifestFromDomain(value)
|
||||
return &body
|
||||
}
|
||||
|
||||
func gameClientBridgeManifestFromDomain(value domain.GameClientBridgeManifest) GameClientBridgeManifestBody {
|
||||
value = domain.CopyGameClientBridgeManifest(value)
|
||||
commands := make([]GameClientBridgeCommandDeclarationBody, len(value.Commands))
|
||||
@@ -1931,7 +1877,7 @@ func JobFromDomain(job domain.Job) JobResponse {
|
||||
State: job.State,
|
||||
Progress: progressFromDomain(job.Progress),
|
||||
ResultRef: job.ResultRef,
|
||||
ExecutionResult: JobExecutionResultResponse{Kind: job.ExecutionResult.Kind, ProcessState: job.ExecutionResult.ProcessState, ExitClassification: job.ExecutionResult.ExitClassification, ExitCode: job.ExecutionResult.ExitCode, Version: job.ExecutionResult.Version, Checksum: job.ExecutionResult.Checksum, SizeBytes: job.ExecutionResult.SizeBytes, AuditSummary: job.ExecutionResult.AuditSummary, SQLiteSchemaProbe: SCUMSchemaProbeResultPtrFromDomain(job.ExecutionResult.SQLiteSchemaProbe), ServerDeploymentEvidence: serverDeploymentEvidenceFromDomain(job.ExecutionResult.ServerDeploymentEvidence)},
|
||||
ExecutionResult: JobExecutionResultResponse{Kind: job.ExecutionResult.Kind, ProcessState: job.ExecutionResult.ProcessState, ExitClassification: job.ExecutionResult.ExitClassification, ExitCode: job.ExecutionResult.ExitCode, Version: job.ExecutionResult.Version, Checksum: job.ExecutionResult.Checksum, SizeBytes: job.ExecutionResult.SizeBytes, AuditSummary: job.ExecutionResult.AuditSummary, ServerDeploymentEvidence: serverDeploymentEvidenceFromDomain(job.ExecutionResult.ServerDeploymentEvidence)},
|
||||
RetryPolicy: JobRetryPolicyResponse{
|
||||
MaxAttempts: job.RetryPolicy.MaxAttempts,
|
||||
InitialBackoffSeconds: job.RetryPolicy.InitialBackoffSeconds,
|
||||
|
||||
@@ -31,233 +31,6 @@ func TestAIProviderResponseExposesOnlySecretPresence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunJobResultRequestParsesSQLiteSchemaProbeEnvelope(t *testing.T) {
|
||||
payload := `{
|
||||
"runEndpointId":"run-local",
|
||||
"sessionToken":"run-session",
|
||||
"jobId":"job-probe",
|
||||
"leaseToken":"lease-probe",
|
||||
"attempt":1,
|
||||
"state":"succeeded",
|
||||
"progress":{"percent":100,"message":"done"},
|
||||
"executionResult":{
|
||||
"kind":"sqlite.schema-probe",
|
||||
"sqliteSchemaProbe":{
|
||||
"requestId":"job-probe",
|
||||
"jobId":"job-probe",
|
||||
"binding":{"serverInstanceId":"server-scum","runBindingId":"runtime-binding-server-scum","runEndpointId":"run-local","pluginId":"server.scum","pluginVersion":"1.0.0","adapterVersion":"scum-live-data-v0","gameVersion":"1.0.0","databaseIdentity":"scum-database"},
|
||||
"status":"compatible",
|
||||
"sourceFingerprint":"sha256:` + strings.Repeat("c", 64) + `",
|
||||
"schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `",
|
||||
"observedAt":"2026-08-12T00:00:00Z",
|
||||
"resultDigest":"sha256:` + strings.Repeat("b", 64) + `",
|
||||
"limits":{"maxObjects":256,"maxColumnsPerObject":128,"maxIndexesPerObject":64,"maxForeignKeys":64,"maxCardinalityReads":64,"maxSampleRows":3,"timeoutMs":5000,"maxResultBytes":524288}
|
||||
}
|
||||
}
|
||||
}`
|
||||
var request RunJobResultRequest
|
||||
if err := json.Unmarshal([]byte(payload), &request); err != nil {
|
||||
t.Fatalf("unmarshal Run job result: %v", err)
|
||||
}
|
||||
domainRequest := request.ToDomain()
|
||||
probe := domainRequest.ExecutionResult.SQLiteSchemaProbe
|
||||
if probe == nil || probe.JobID != "job-probe" || probe.Binding.DatabaseIdentity != "scum-database" || probe.SourceFingerprint != "sha256:"+strings.Repeat("c", 64) || probe.ResultDigest != "sha256:"+strings.Repeat("b", 64) {
|
||||
t.Fatalf("sqliteSchemaProbe envelope did not parse: %+v", probe)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunJobResultRequestParsesSQLiteTemplateEnvelope(t *testing.T) {
|
||||
payload := `{
|
||||
"runEndpointId":"run-local",
|
||||
"sessionToken":"run-session",
|
||||
"jobId":"job-query",
|
||||
"leaseToken":"lease-query",
|
||||
"attempt":1,
|
||||
"state":"succeeded",
|
||||
"progress":{"percent":100,"message":"done"},
|
||||
"executionResult":{
|
||||
"kind":"sqlite.template-query",
|
||||
"sqliteTemplate":{
|
||||
"requestId":"request-query",
|
||||
"jobId":"job-query",
|
||||
"binding":{"serverInstanceId":"server-scum","runBindingId":"runtime-binding-server-scum","runEndpointId":"run-local","pluginId":"server.scum","pluginVersion":"1.0.0","adapterVersion":"scum-live-data-v0","gameVersion":"1.0.0","databaseIdentity":"scum-database"},
|
||||
"status":"succeeded",
|
||||
"capability":"players.read",
|
||||
"targetKey":"scum-database",
|
||||
"templateKey":"players.active.v1",
|
||||
"adapterVersion":"scum-live-data-v0",
|
||||
"schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `",
|
||||
"assetDigest":"sha256:` + strings.Repeat("d", 64) + `",
|
||||
"parameterDigest":"sha256:` + strings.Repeat("e", 64) + `",
|
||||
"sourceFingerprint":"sha256:` + strings.Repeat("c", 64) + `",
|
||||
"observedAt":"2026-08-13T00:00:00Z",
|
||||
"resultDigest":"sha256:` + strings.Repeat("b", 64) + `",
|
||||
"rowCount":1,
|
||||
"rows":[{"externalPlayerId":"player-redacted","fame":12.5,"online":true,"squadId":null}],
|
||||
"limits":{"maxParameters":64,"maxRows":500,"timeoutMs":5000,"busyTimeoutMs":250,"maxResultBytes":1048576}
|
||||
}
|
||||
}
|
||||
}`
|
||||
var request RunJobResultRequest
|
||||
if err := json.Unmarshal([]byte(payload), &request); err != nil {
|
||||
t.Fatalf("unmarshal Run job result: %v", err)
|
||||
}
|
||||
domainRequest := request.ToDomain()
|
||||
result := domainRequest.ExecutionResult.SQLiteTemplate
|
||||
if result == nil || result.TemplateKey != "players.active.v1" || result.AssetDigest != "sha256:"+strings.Repeat("d", 64) || result.RowCount != 1 || result.Rows[0]["fame"].(float64) != 12.5 {
|
||||
t.Fatalf("sqliteTemplate envelope did not parse: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunJobResultRequestParsesTypedRCONTemplateEnvelope(t *testing.T) {
|
||||
payload := `{
|
||||
"runEndpointId":"run-local",
|
||||
"sessionToken":"run-session",
|
||||
"jobId":"job-rcon",
|
||||
"leaseToken":"lease-rcon",
|
||||
"attempt":1,
|
||||
"state":"succeeded",
|
||||
"progress":{"percent":100,"message":"done"},
|
||||
"executionResult":{
|
||||
"kind":"rcon.template-command",
|
||||
"rconTemplate":{
|
||||
"requestId":"request-rcon",
|
||||
"jobId":"job-rcon",
|
||||
"binding":{"serverInstanceId":"server-scum","runBindingId":"runtime-binding-server-scum","runEndpointId":"run-local","pluginId":"server.scum","pluginVersion":"1.0.0","adapterVersion":"scum-live-data-v0","gameVersion":"1.0.0","databaseIdentity":"scum-database"},
|
||||
"status":"succeeded",
|
||||
"capability":"economy-command.write",
|
||||
"transportKey":"scum-rcon",
|
||||
"targetKey":"scum-rcon",
|
||||
"templateKey":"economy.fame.set.v1",
|
||||
"adapterVersion":"scum-live-data-v0",
|
||||
"schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `",
|
||||
"assetDigest":"sha256:` + strings.Repeat("d", 64) + `",
|
||||
"payloadDigest":"sha256:` + strings.Repeat("e", 64) + `",
|
||||
"confirmationDigest":"sha256:` + strings.Repeat("f", 64) + `",
|
||||
"targetIdentityDigest":"sha256:` + strings.Repeat("c", 64) + `",
|
||||
"observedAt":"2026-08-13T00:00:00Z",
|
||||
"resultDigest":"sha256:` + strings.Repeat("b", 64) + `",
|
||||
"responseDigest":"sha256:` + strings.Repeat("1", 64) + `",
|
||||
"confirmationStatus":"confirmed",
|
||||
"confirmationDigestId":"sha256:` + strings.Repeat("2", 64) + `",
|
||||
"safeSummary":"confirmed by declared readback",
|
||||
"limits":{"maxPayloadBytes":2048,"timeoutMs":5000,"maxResponseBytes":16384,"maxConfirmRecords":16}
|
||||
}
|
||||
}
|
||||
}`
|
||||
var request RunJobResultRequest
|
||||
if err := json.Unmarshal([]byte(payload), &request); err != nil {
|
||||
t.Fatalf("unmarshal Run job result: %v", err)
|
||||
}
|
||||
domainRequest := request.ToDomain()
|
||||
result := domainRequest.ExecutionResult.RCONTemplate
|
||||
if result == nil || result.TemplateKey != "economy.fame.set.v1" || result.PayloadDigest != "sha256:"+strings.Repeat("e", 64) || result.ConfirmationStatus != domain.SCUMRCONConfirmationConfirmed {
|
||||
t.Fatalf("rconTemplate envelope did not parse: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunJobResultRequestParsesGuardedMutationEnvelope(t *testing.T) {
|
||||
payload := `{
|
||||
"runEndpointId":"run-local",
|
||||
"sessionToken":"run-session",
|
||||
"jobId":"job-mutation",
|
||||
"leaseToken":"lease-mutation",
|
||||
"attempt":1,
|
||||
"state":"succeeded",
|
||||
"progress":{"percent":100,"message":"done"},
|
||||
"executionResult":{
|
||||
"kind":"sqlite.guarded-mutation",
|
||||
"guardedMutation":{
|
||||
"requestId":"request-mutation",
|
||||
"jobId":"job-mutation",
|
||||
"binding":{"serverInstanceId":"server-scum","runBindingId":"runtime-binding-server-scum","runEndpointId":"run-local","pluginId":"server.scum","pluginVersion":"1.0.0","adapterVersion":"scum-live-data-v0","gameVersion":"1.0.0","databaseIdentity":"scum-database"},
|
||||
"status":"succeeded",
|
||||
"capability":"profile-xml.write",
|
||||
"targetKey":"scum-mutation-db",
|
||||
"templateKey":"profile.attributes.patch.v1",
|
||||
"adapterVersion":"scum-live-data-v0",
|
||||
"schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `",
|
||||
"assetDigest":"sha256:` + strings.Repeat("d", 64) + `",
|
||||
"sourceFingerprint":"sha256:` + strings.Repeat("c", 64) + `",
|
||||
"targetIdentityDigest":"sha256:` + strings.Repeat("1", 64) + `",
|
||||
"expectedRowDigest":"sha256:` + strings.Repeat("2", 64) + `",
|
||||
"expectedValueDigest":"sha256:` + strings.Repeat("3", 64) + `",
|
||||
"expectedXmlDigest":"sha256:` + strings.Repeat("4", 64) + `",
|
||||
"patchDigest":"sha256:` + strings.Repeat("5", 64) + `",
|
||||
"backupEvidenceDigest":"sha256:` + strings.Repeat("6", 64) + `",
|
||||
"offlineEvidenceDigest":"sha256:` + strings.Repeat("7", 64) + `",
|
||||
"dangerConfirmationDigest":"sha256:` + strings.Repeat("8", 64) + `",
|
||||
"readbackExpectationDigest":"sha256:` + strings.Repeat("9", 64) + `",
|
||||
"observedAt":"2026-08-13T00:00:00Z",
|
||||
"resultDigest":"sha256:` + strings.Repeat("b", 64) + `",
|
||||
"beforeDigest":"sha256:` + strings.Repeat("a", 64) + `",
|
||||
"afterDigest":"sha256:` + strings.Repeat("b", 64) + `",
|
||||
"readbackDigest":"sha256:` + strings.Repeat("c", 64) + `",
|
||||
"affectedRows":1,
|
||||
"readbackStatus":"confirmed",
|
||||
"safeSummary":"confirmed by declared readback",
|
||||
"limits":{"maxPayloadBytes":4096,"timeoutMs":5000,"busyTimeoutMs":250,"maxReadbackBytes":16384,"maxAffectedRows":1}
|
||||
}
|
||||
}
|
||||
}`
|
||||
var request RunJobResultRequest
|
||||
if err := json.Unmarshal([]byte(payload), &request); err != nil {
|
||||
t.Fatalf("unmarshal Run job result: %v", err)
|
||||
}
|
||||
domainRequest := request.ToDomain()
|
||||
result := domainRequest.ExecutionResult.GuardedMutation
|
||||
if result == nil || result.TemplateKey != "profile.attributes.patch.v1" || result.PatchDigest != "sha256:"+strings.Repeat("5", 64) || result.AffectedRows != 1 || result.ReadbackStatus != domain.SCUMMutationReadbackConfirmed {
|
||||
t.Fatalf("guardedMutation envelope did not parse: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunJobResultRequestParsesParsedLogBatchEnvelope(t *testing.T) {
|
||||
payload := `{
|
||||
"runEndpointId":"run-local",
|
||||
"sessionToken":"run-session",
|
||||
"jobId":"job-log",
|
||||
"leaseToken":"lease-log",
|
||||
"attempt":1,
|
||||
"state":"succeeded",
|
||||
"progress":{"percent":100,"message":"done"},
|
||||
"executionResult":{
|
||||
"kind":"log.parsed-events",
|
||||
"parsedLogBatch":{
|
||||
"requestId":"request-log",
|
||||
"jobId":"job-log",
|
||||
"binding":{"serverInstanceId":"server-scum","runBindingId":"runtime-binding-server-scum","runEndpointId":"run-local","pluginId":"server.scum","pluginVersion":"1.0.0","adapterVersion":"scum-live-data-v0","gameVersion":"1.0.0","databaseIdentity":"scum-database"},
|
||||
"status":"succeeded",
|
||||
"sourceKey":"scum-login-events",
|
||||
"streamKey":"scum.login",
|
||||
"parserKey":"scum-login-log-login-parser",
|
||||
"parserVersion":"scum-login-log-v1",
|
||||
"adapterVersion":"scum-live-data-v0",
|
||||
"assetDigest":"sha256:` + strings.Repeat("d", 64) + `",
|
||||
"parserDigest":"sha256:` + strings.Repeat("e", 64) + `",
|
||||
"observedAt":"2026-08-13T00:00:00Z",
|
||||
"resultDigest":"sha256:` + strings.Repeat("b", 64) + `",
|
||||
"firstCursor":{"sourceIdentityDigest":"sha256:` + strings.Repeat("1", 64) + `","streamGeneration":"sha256:` + strings.Repeat("2", 64) + `","sequence":7},
|
||||
"lastCursor":{"sourceIdentityDigest":"sha256:` + strings.Repeat("1", 64) + `","streamGeneration":"sha256:` + strings.Repeat("2", 64) + `","sequence":7},
|
||||
"tailState":"rotated",
|
||||
"replay":true,
|
||||
"eventCount":1,
|
||||
"events":[{"eventType":"scum.login","occurredAt":"2026-08-13T00:00:00Z","cursor":{"sourceIdentityDigest":"sha256:` + strings.Repeat("1", 64) + `","streamGeneration":"sha256:` + strings.Repeat("2", 64) + `","sequence":7},"logicalEventDigest":"sha256:` + strings.Repeat("3", 64) + `","eventDigest":"sha256:` + strings.Repeat("4", 64) + `","payloadDigest":"sha256:` + strings.Repeat("5", 64) + `","payload":{"externalPlayerId":"player-redacted","displayName":"Known Player"}}],
|
||||
"safeSummary":"one sanitized login event parsed from declared source",
|
||||
"limits":{"maxEvents":256,"maxPayloadBytes":16384,"maxLineBytes":4096,"maxResultBytes":262144}
|
||||
}
|
||||
}
|
||||
}`
|
||||
var request RunJobResultRequest
|
||||
if err := json.Unmarshal([]byte(payload), &request); err != nil {
|
||||
t.Fatalf("unmarshal Run parsed log result: %v", err)
|
||||
}
|
||||
domainRequest := request.ToDomain()
|
||||
result := domainRequest.ExecutionResult.ParsedLogBatch
|
||||
if result == nil || result.ParserKey != "scum-login-log-login-parser" || result.ParserDigest != "sha256:"+strings.Repeat("e", 64) || result.EventCount != 1 || result.Events[0].LogicalEventDigest != "sha256:"+strings.Repeat("3", 64) {
|
||||
t.Fatalf("parsedLogBatch envelope did not parse: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIProviderFromDomainCopiesModels(t *testing.T) {
|
||||
provider := domain.AIProvider{
|
||||
ID: "ai.openai",
|
||||
|
||||
@@ -127,18 +127,6 @@ type RuntimeTransportProfileBody struct {
|
||||
Capabilities []string `json:"capabilities"`
|
||||
}
|
||||
|
||||
type RuntimeDataTargetBody struct {
|
||||
Key string `json:"key"`
|
||||
Kind string `json:"kind"`
|
||||
TransportKey string `json:"transportKey"`
|
||||
SourceRootKey string `json:"sourceRootKey"`
|
||||
SourcePath string `json:"sourcePath"`
|
||||
WorkspaceKey string `json:"workspaceKey"`
|
||||
RefreshPolicy string `json:"refreshPolicy"`
|
||||
MaxBytes int64 `json:"maxBytes,omitempty"`
|
||||
Platforms []string `json:"platforms,omitempty"`
|
||||
}
|
||||
|
||||
type RuntimeRepositoryBody struct {
|
||||
URL string `json:"url"`
|
||||
RevisionPolicy string `json:"revisionPolicy"`
|
||||
@@ -275,7 +263,6 @@ type GamePluginRuntimeProfilesBody struct {
|
||||
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
|
||||
LogEvents []RuntimeLogEventBody `json:"logEvents,omitempty"`
|
||||
TransportProfiles []RuntimeTransportProfileBody `json:"transportProfiles,omitempty"`
|
||||
DataTargets []RuntimeDataTargetBody `json:"dataTargets,omitempty"`
|
||||
ClientManagers []RuntimeClientManagerProfileBody `json:"clientManagers,omitempty"`
|
||||
DLLExtensions []RuntimeDLLExtensionProfileBody `json:"dllExtensions,omitempty"`
|
||||
}
|
||||
@@ -342,9 +329,6 @@ func (body GamePluginRuntimeProfilesBody) ToDomain() domain.GamePluginRuntimePro
|
||||
for _, item := range body.TransportProfiles {
|
||||
profiles.TransportProfiles = append(profiles.TransportProfiles, domain.RuntimeTransportProfile{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, Capabilities: domain.CopyStringSlice(item.Capabilities)})
|
||||
}
|
||||
for _, item := range body.DataTargets {
|
||||
profiles.DataTargets = append(profiles.DataTargets, domain.RuntimeDataTarget{Key: item.Key, Kind: item.Kind, TransportKey: item.TransportKey, SourceRootKey: item.SourceRootKey, SourcePath: item.SourcePath, WorkspaceKey: item.WorkspaceKey, RefreshPolicy: item.RefreshPolicy, MaxBytes: item.MaxBytes, Platforms: domain.CopyStringSlice(item.Platforms)})
|
||||
}
|
||||
for _, item := range body.ClientManagers {
|
||||
manager := domain.RuntimeClientManagerProfile{
|
||||
Key: item.Key, DisplayName: item.DisplayName, Version: item.Version,
|
||||
|
||||
@@ -1,661 +0,0 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
type SCUMBindingIdentityDTO struct {
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
RunBindingID string `json:"runBindingId"`
|
||||
RunEndpointID string `json:"runEndpointId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
PluginVersion string `json:"pluginVersion"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
GameVersion string `json:"gameVersion"`
|
||||
DatabaseIdentity string `json:"databaseIdentity"`
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeBoundsDTO struct {
|
||||
MaxObjects int `json:"maxObjects"`
|
||||
MaxColumnsPerObject int `json:"maxColumnsPerObject"`
|
||||
MaxIndexesPerObject int `json:"maxIndexesPerObject"`
|
||||
MaxForeignKeys int `json:"maxForeignKeys"`
|
||||
MaxCardinalityReads int `json:"maxCardinalityReads"`
|
||||
MaxSampleRows int `json:"maxSampleRows"`
|
||||
TimeoutMS int `json:"timeoutMs"`
|
||||
MaxResultBytes int `json:"maxResultBytes"`
|
||||
}
|
||||
|
||||
type SCUMSQLiteTemplateBoundsDTO struct {
|
||||
MaxParameters int `json:"maxParameters"`
|
||||
MaxRows int `json:"maxRows"`
|
||||
TimeoutMS int `json:"timeoutMs"`
|
||||
BusyTimeoutMS int `json:"busyTimeoutMs"`
|
||||
MaxResultBytes int `json:"maxResultBytes"`
|
||||
}
|
||||
|
||||
type SCUMTypedRCONTemplateBoundsDTO struct {
|
||||
MaxPayloadBytes int `json:"maxPayloadBytes"`
|
||||
TimeoutMS int `json:"timeoutMs"`
|
||||
MaxResponseBytes int `json:"maxResponseBytes"`
|
||||
MaxConfirmRecords int `json:"maxConfirmRecords"`
|
||||
}
|
||||
|
||||
type SCUMGuardedMutationBoundsDTO struct {
|
||||
MaxPayloadBytes int `json:"maxPayloadBytes"`
|
||||
TimeoutMS int `json:"timeoutMs"`
|
||||
BusyTimeoutMS int `json:"busyTimeoutMs"`
|
||||
MaxReadbackBytes int `json:"maxReadbackBytes"`
|
||||
MaxAffectedRows int `json:"maxAffectedRows"`
|
||||
}
|
||||
|
||||
type SCUMParsedLogBatchBoundsDTO struct {
|
||||
MaxEvents int `json:"maxEvents"`
|
||||
MaxPayloadBytes int `json:"maxPayloadBytes"`
|
||||
MaxLineBytes int `json:"maxLineBytes"`
|
||||
MaxResultBytes int `json:"maxResultBytes"`
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeRequestDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Bounds SCUMSchemaProbeBoundsDTO `json:"bounds"`
|
||||
RequestedAt time.Time `json:"requestedAt"`
|
||||
}
|
||||
|
||||
type SCUMSQLiteTemplateRequestDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Capability string `json:"capability"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
RequiredSchemaFingerprint string `json:"requiredSchemaFingerprint"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
ParameterDigest string `json:"parameterDigest"`
|
||||
Parameters map[string]any `json:"parameters,omitempty"`
|
||||
Bounds SCUMSQLiteTemplateBoundsDTO `json:"bounds"`
|
||||
RequestedAt time.Time `json:"requestedAt"`
|
||||
}
|
||||
|
||||
type SCUMTypedRCONTemplateRequestDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Capability string `json:"capability"`
|
||||
TransportKey string `json:"transportKey"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
RequiredSchemaFingerprint string `json:"requiredSchemaFingerprint,omitempty"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
PayloadDigest string `json:"payloadDigest"`
|
||||
ConfirmationDigest string `json:"confirmationDigest"`
|
||||
TargetIdentityDigest string `json:"targetIdentityDigest"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
ReviewReason string `json:"reviewReason"`
|
||||
Bounds SCUMTypedRCONTemplateBoundsDTO `json:"bounds"`
|
||||
RequestedAt time.Time `json:"requestedAt"`
|
||||
}
|
||||
|
||||
type SCUMGuardedMutationRequestDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Capability string `json:"capability"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
RequiredSchemaFingerprint string `json:"requiredSchemaFingerprint"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
TargetIdentityDigest string `json:"targetIdentityDigest"`
|
||||
ExpectedRowDigest string `json:"expectedRowDigest"`
|
||||
ExpectedValueDigest string `json:"expectedValueDigest"`
|
||||
ExpectedXMLDigest string `json:"expectedXmlDigest"`
|
||||
PatchDigest string `json:"patchDigest"`
|
||||
BackupEvidenceDigest string `json:"backupEvidenceDigest"`
|
||||
OfflineEvidenceDigest string `json:"offlineEvidenceDigest"`
|
||||
DangerConfirmationDigest string `json:"dangerConfirmationDigest"`
|
||||
ReadbackExpectationDigest string `json:"readbackExpectationDigest"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
ReviewReason string `json:"reviewReason"`
|
||||
Bounds SCUMGuardedMutationBoundsDTO `json:"bounds"`
|
||||
RequestedAt time.Time `json:"requestedAt"`
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeDispatchRequest struct {
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeDispatchResponse struct {
|
||||
ProbeRequest SCUMSchemaProbeRequestDTO `json:"probeRequest"`
|
||||
QueuedJob RemoteAdapterResponse `json:"queuedJob"`
|
||||
}
|
||||
|
||||
type SCUMSafeErrorDTO struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Retryable bool `json:"retryable"`
|
||||
}
|
||||
|
||||
type SCUMSchemaColumnEvidenceDTO struct {
|
||||
NameFingerprint string `json:"nameFingerprint"`
|
||||
DeclaredType string `json:"declaredType"`
|
||||
Nullable *bool `json:"nullable,omitempty"`
|
||||
PrimaryKey bool `json:"primaryKey"`
|
||||
Ordinal int `json:"ordinal"`
|
||||
}
|
||||
|
||||
type SCUMSchemaIndexEvidenceDTO struct {
|
||||
NameFingerprint string `json:"nameFingerprint"`
|
||||
Unique bool `json:"unique"`
|
||||
ColumnHashes []string `json:"columnHashes"`
|
||||
}
|
||||
|
||||
type SCUMSchemaForeignKeyEvidenceDTO struct {
|
||||
FromColumnHash string `json:"fromColumnHash"`
|
||||
ToObjectHash string `json:"toObjectHash"`
|
||||
ToColumnHash string `json:"toColumnHash"`
|
||||
}
|
||||
|
||||
type SCUMSchemaObjectEvidenceDTO struct {
|
||||
ObjectHash string `json:"objectHash"`
|
||||
Kind string `json:"kind"`
|
||||
NameFingerprint string `json:"nameFingerprint"`
|
||||
DeclaredColumns []SCUMSchemaColumnEvidenceDTO `json:"declaredColumns"`
|
||||
Indexes []SCUMSchemaIndexEvidenceDTO `json:"indexes"`
|
||||
ForeignKeys []SCUMSchemaForeignKeyEvidenceDTO `json:"foreignKeys"`
|
||||
ApproximateRows *int64 `json:"approximateRows,omitempty"`
|
||||
SampleFingerprints []string `json:"sampleFingerprints,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeResultDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Status string `json:"status"`
|
||||
SourceFingerprint string `json:"sourceFingerprint,omitempty"`
|
||||
SchemaFingerprint string `json:"schemaFingerprint,omitempty"`
|
||||
ObservedAt time.Time `json:"observedAt"`
|
||||
ResultDigest string `json:"resultDigest,omitempty"`
|
||||
Objects []SCUMSchemaObjectEvidenceDTO `json:"objects,omitempty"`
|
||||
SafeError SCUMSafeErrorDTO `json:"safeError,omitempty"`
|
||||
Limits SCUMSchemaProbeBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type SCUMSQLiteTemplateResultDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Status string `json:"status"`
|
||||
Capability string `json:"capability"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
SchemaFingerprint string `json:"schemaFingerprint,omitempty"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
ParameterDigest string `json:"parameterDigest"`
|
||||
SourceFingerprint string `json:"sourceFingerprint,omitempty"`
|
||||
ObservedAt time.Time `json:"observedAt"`
|
||||
ResultDigest string `json:"resultDigest,omitempty"`
|
||||
RowCount int `json:"rowCount"`
|
||||
Rows []map[string]any `json:"rows,omitempty"`
|
||||
Truncated bool `json:"truncated,omitempty"`
|
||||
SafeError SCUMSafeErrorDTO `json:"safeError,omitempty"`
|
||||
Limits SCUMSQLiteTemplateBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type SCUMTypedRCONTemplateResultDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Status string `json:"status"`
|
||||
Capability string `json:"capability"`
|
||||
TransportKey string `json:"transportKey"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
SchemaFingerprint string `json:"schemaFingerprint,omitempty"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
PayloadDigest string `json:"payloadDigest"`
|
||||
ConfirmationDigest string `json:"confirmationDigest"`
|
||||
TargetIdentityDigest string `json:"targetIdentityDigest"`
|
||||
ObservedAt time.Time `json:"observedAt"`
|
||||
ResultDigest string `json:"resultDigest"`
|
||||
ResponseDigest string `json:"responseDigest,omitempty"`
|
||||
ConfirmationStatus string `json:"confirmationStatus"`
|
||||
ConfirmationDigestID string `json:"confirmationDigestId,omitempty"`
|
||||
SafeSummary string `json:"safeSummary,omitempty"`
|
||||
SafeError SCUMSafeErrorDTO `json:"safeError,omitempty"`
|
||||
Limits SCUMTypedRCONTemplateBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type SCUMGuardedMutationResultDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Status string `json:"status"`
|
||||
Capability string `json:"capability"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
SchemaFingerprint string `json:"schemaFingerprint"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
SourceFingerprint string `json:"sourceFingerprint,omitempty"`
|
||||
TargetIdentityDigest string `json:"targetIdentityDigest"`
|
||||
ExpectedRowDigest string `json:"expectedRowDigest"`
|
||||
ExpectedValueDigest string `json:"expectedValueDigest"`
|
||||
ExpectedXMLDigest string `json:"expectedXmlDigest"`
|
||||
PatchDigest string `json:"patchDigest"`
|
||||
BackupEvidenceDigest string `json:"backupEvidenceDigest"`
|
||||
OfflineEvidenceDigest string `json:"offlineEvidenceDigest"`
|
||||
DangerConfirmationDigest string `json:"dangerConfirmationDigest"`
|
||||
ReadbackExpectationDigest string `json:"readbackExpectationDigest"`
|
||||
ObservedAt time.Time `json:"observedAt"`
|
||||
ResultDigest string `json:"resultDigest"`
|
||||
BeforeDigest string `json:"beforeDigest,omitempty"`
|
||||
AfterDigest string `json:"afterDigest,omitempty"`
|
||||
ReadbackDigest string `json:"readbackDigest,omitempty"`
|
||||
AffectedRows int `json:"affectedRows"`
|
||||
ReadbackStatus string `json:"readbackStatus"`
|
||||
SafeSummary string `json:"safeSummary,omitempty"`
|
||||
SafeError SCUMSafeErrorDTO `json:"safeError,omitempty"`
|
||||
Limits SCUMGuardedMutationBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type SCUMParsedLogCursorDTO struct {
|
||||
SourceIdentityDigest string `json:"sourceIdentityDigest"`
|
||||
StreamGeneration string `json:"streamGeneration"`
|
||||
Sequence uint64 `json:"sequence"`
|
||||
}
|
||||
|
||||
type SCUMParsedLogEventDTO struct {
|
||||
EventType string `json:"eventType"`
|
||||
OccurredAt time.Time `json:"occurredAt"`
|
||||
Cursor SCUMParsedLogCursorDTO `json:"cursor"`
|
||||
LogicalEventDigest string `json:"logicalEventDigest"`
|
||||
EventDigest string `json:"eventDigest"`
|
||||
PayloadDigest string `json:"payloadDigest"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMParsedLogBatchResultDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Status string `json:"status"`
|
||||
SourceKey string `json:"sourceKey"`
|
||||
StreamKey string `json:"streamKey"`
|
||||
ParserKey string `json:"parserKey"`
|
||||
ParserVersion string `json:"parserVersion"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
ParserDigest string `json:"parserDigest"`
|
||||
ObservedAt time.Time `json:"observedAt"`
|
||||
ResultDigest string `json:"resultDigest"`
|
||||
FirstCursor SCUMParsedLogCursorDTO `json:"firstCursor"`
|
||||
LastCursor SCUMParsedLogCursorDTO `json:"lastCursor"`
|
||||
TailState string `json:"tailState"`
|
||||
PartialLineBuffered bool `json:"partialLineBuffered,omitempty"`
|
||||
Replay bool `json:"replay,omitempty"`
|
||||
EventCount int `json:"eventCount"`
|
||||
Events []SCUMParsedLogEventDTO `json:"events,omitempty"`
|
||||
SafeSummary string `json:"safeSummary,omitempty"`
|
||||
SafeError SCUMSafeErrorDTO `json:"safeError,omitempty"`
|
||||
Limits SCUMParsedLogBatchBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type SCUMCapabilityGateDTO struct {
|
||||
Capability string `json:"capability"`
|
||||
State string `json:"state"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ReasonCode string `json:"reasonCode"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type SCUMCapabilityNegotiationDTO struct {
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
RunEndpointID string `json:"runEndpointId"`
|
||||
RunBindingID string `json:"runBindingId,omitempty"`
|
||||
PluginID string `json:"pluginId"`
|
||||
PluginVersion string `json:"pluginVersion"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
GameVersion string `json:"gameVersion,omitempty"`
|
||||
DatabaseIdentity string `json:"databaseIdentity"`
|
||||
ProbeExecutorAvailable bool `json:"probeExecutorAvailable"`
|
||||
EvaluatedAt time.Time `json:"evaluatedAt"`
|
||||
Gates []SCUMCapabilityGateDTO `json:"gates"`
|
||||
}
|
||||
|
||||
func SCUMSchemaProbeRequestToDomain(value SCUMSchemaProbeRequestDTO) domain.SCUMSchemaProbeRequest {
|
||||
return domain.SCUMSchemaProbeRequest{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Bounds: scumProbeBoundsToDomain(value.Bounds), RequestedAt: value.RequestedAt}
|
||||
}
|
||||
|
||||
func SCUMSchemaProbeRequestFromDomain(value domain.SCUMSchemaProbeRequest) SCUMSchemaProbeRequestDTO {
|
||||
return SCUMSchemaProbeRequestDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Bounds: scumProbeBoundsFromDomain(value.Bounds), RequestedAt: value.RequestedAt}
|
||||
}
|
||||
|
||||
func SCUMSchemaProbeRequestPtrFromDomain(value *domain.SCUMSchemaProbeRequest) *SCUMSchemaProbeRequestDTO {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMSchemaProbeRequestFromDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMSQLiteTemplateRequestToDomain(value SCUMSQLiteTemplateRequestDTO) domain.SCUMSQLiteTemplateRequest {
|
||||
return domain.SCUMSQLiteTemplateRequest{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Capability: domain.SCUMDataCapability(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, ParameterDigest: value.ParameterDigest, Parameters: domain.CopySCUMValueMap(value.Parameters), Bounds: scumSQLiteTemplateBoundsToDomain(value.Bounds), RequestedAt: value.RequestedAt}
|
||||
}
|
||||
|
||||
func SCUMSQLiteTemplateRequestFromDomain(value domain.SCUMSQLiteTemplateRequest) SCUMSQLiteTemplateRequestDTO {
|
||||
return SCUMSQLiteTemplateRequestDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Capability: string(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, ParameterDigest: value.ParameterDigest, Parameters: domain.CopySCUMValueMap(value.Parameters), Bounds: scumSQLiteTemplateBoundsFromDomain(value.Bounds), RequestedAt: value.RequestedAt}
|
||||
}
|
||||
|
||||
func SCUMSQLiteTemplateRequestPtrFromDomain(value *domain.SCUMSQLiteTemplateRequest) *SCUMSQLiteTemplateRequestDTO {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMSQLiteTemplateRequestFromDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMTypedRCONTemplateRequestToDomain(value SCUMTypedRCONTemplateRequestDTO) domain.SCUMTypedRCONTemplateRequest {
|
||||
return domain.SCUMTypedRCONTemplateRequest{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Capability: domain.SCUMDataCapability(value.Capability), TransportKey: value.TransportKey, TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, PayloadDigest: value.PayloadDigest, ConfirmationDigest: value.ConfirmationDigest, TargetIdentityDigest: value.TargetIdentityDigest, IdempotencyKey: value.IdempotencyKey, Payload: domain.CopySCUMValueMap(value.Payload), ReviewReason: value.ReviewReason, Bounds: scumTypedRCONTemplateBoundsToDomain(value.Bounds), RequestedAt: value.RequestedAt}
|
||||
}
|
||||
|
||||
func SCUMTypedRCONTemplateRequestFromDomain(value domain.SCUMTypedRCONTemplateRequest) SCUMTypedRCONTemplateRequestDTO {
|
||||
return SCUMTypedRCONTemplateRequestDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Capability: string(value.Capability), TransportKey: value.TransportKey, TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, PayloadDigest: value.PayloadDigest, ConfirmationDigest: value.ConfirmationDigest, TargetIdentityDigest: value.TargetIdentityDigest, IdempotencyKey: value.IdempotencyKey, Payload: domain.CopySCUMValueMap(value.Payload), ReviewReason: value.ReviewReason, Bounds: scumTypedRCONTemplateBoundsFromDomain(value.Bounds), RequestedAt: value.RequestedAt}
|
||||
}
|
||||
|
||||
func SCUMTypedRCONTemplateRequestPtrFromDomain(value *domain.SCUMTypedRCONTemplateRequest) *SCUMTypedRCONTemplateRequestDTO {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMTypedRCONTemplateRequestFromDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationRequestToDomain(value SCUMGuardedMutationRequestDTO) domain.SCUMGuardedMutationRequest {
|
||||
return domain.SCUMGuardedMutationRequest{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Capability: domain.SCUMDataCapability(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, TargetIdentityDigest: value.TargetIdentityDigest, ExpectedRowDigest: value.ExpectedRowDigest, ExpectedValueDigest: value.ExpectedValueDigest, ExpectedXMLDigest: value.ExpectedXMLDigest, PatchDigest: value.PatchDigest, BackupEvidenceDigest: value.BackupEvidenceDigest, OfflineEvidenceDigest: value.OfflineEvidenceDigest, DangerConfirmationDigest: value.DangerConfirmationDigest, ReadbackExpectationDigest: value.ReadbackExpectationDigest, IdempotencyKey: value.IdempotencyKey, Payload: domain.CopySCUMValueMap(value.Payload), ReviewReason: value.ReviewReason, Bounds: scumGuardedMutationBoundsToDomain(value.Bounds), RequestedAt: value.RequestedAt}
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationRequestFromDomain(value domain.SCUMGuardedMutationRequest) SCUMGuardedMutationRequestDTO {
|
||||
return SCUMGuardedMutationRequestDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Capability: string(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, TargetIdentityDigest: value.TargetIdentityDigest, ExpectedRowDigest: value.ExpectedRowDigest, ExpectedValueDigest: value.ExpectedValueDigest, ExpectedXMLDigest: value.ExpectedXMLDigest, PatchDigest: value.PatchDigest, BackupEvidenceDigest: value.BackupEvidenceDigest, OfflineEvidenceDigest: value.OfflineEvidenceDigest, DangerConfirmationDigest: value.DangerConfirmationDigest, ReadbackExpectationDigest: value.ReadbackExpectationDigest, IdempotencyKey: value.IdempotencyKey, Payload: domain.CopySCUMValueMap(value.Payload), ReviewReason: value.ReviewReason, Bounds: scumGuardedMutationBoundsFromDomain(value.Bounds), RequestedAt: value.RequestedAt}
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationRequestPtrFromDomain(value *domain.SCUMGuardedMutationRequest) *SCUMGuardedMutationRequestDTO {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMGuardedMutationRequestFromDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func runSQLiteSchemaProbeRequestPtrFromDomain(value *domain.SCUMSchemaProbeRequest) *RunSQLiteSchemaProbeRequestBody {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return &RunSQLiteSchemaProbeRequestBody{RequestID: value.RequestID, Binding: scumBindingIdentityFromDomain(value.Binding), Limits: scumProbeBoundsFromDomain(value.Bounds)}
|
||||
}
|
||||
|
||||
func runSQLiteTemplateRequestPtrFromDomain(value *domain.SCUMSQLiteTemplateRequest) *RunSQLiteTemplateRequestBody {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return &RunSQLiteTemplateRequestBody{RequestID: value.RequestID, Binding: scumBindingIdentityFromDomain(value.Binding), Capability: string(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, ParameterDigest: value.ParameterDigest, Parameters: domain.CopySCUMValueMap(value.Parameters), Limits: scumSQLiteTemplateBoundsFromDomain(value.Bounds)}
|
||||
}
|
||||
|
||||
func runTypedRCONTemplateRequestPtrFromDomain(value *domain.SCUMTypedRCONTemplateRequest) *RunTypedRCONTemplateRequestBody {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return &RunTypedRCONTemplateRequestBody{RequestID: value.RequestID, Binding: scumBindingIdentityFromDomain(value.Binding), Capability: string(value.Capability), TransportKey: value.TransportKey, TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, PayloadDigest: value.PayloadDigest, ConfirmationDigest: value.ConfirmationDigest, TargetIdentityDigest: value.TargetIdentityDigest, IdempotencyKey: value.IdempotencyKey, Payload: domain.CopySCUMValueMap(value.Payload), ReviewReason: value.ReviewReason, Limits: scumTypedRCONTemplateBoundsFromDomain(value.Bounds)}
|
||||
}
|
||||
|
||||
func runGuardedMutationRequestPtrFromDomain(value *domain.SCUMGuardedMutationRequest) *RunGuardedMutationRequestBody {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return &RunGuardedMutationRequestBody{RequestID: value.RequestID, Binding: scumBindingIdentityFromDomain(value.Binding), Capability: string(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, TargetIdentityDigest: value.TargetIdentityDigest, ExpectedRowDigest: value.ExpectedRowDigest, ExpectedValueDigest: value.ExpectedValueDigest, ExpectedXMLDigest: value.ExpectedXMLDigest, PatchDigest: value.PatchDigest, BackupEvidenceDigest: value.BackupEvidenceDigest, OfflineEvidenceDigest: value.OfflineEvidenceDigest, DangerConfirmationDigest: value.DangerConfirmationDigest, ReadbackExpectationDigest: value.ReadbackExpectationDigest, IdempotencyKey: value.IdempotencyKey, Payload: domain.CopySCUMValueMap(value.Payload), ReviewReason: value.ReviewReason, Limits: scumGuardedMutationBoundsFromDomain(value.Bounds)}
|
||||
}
|
||||
|
||||
func SCUMSchemaProbeDispatchFromDomain(request domain.SCUMSchemaProbeRequest, queued domain.RemoteAdapterResult) SCUMSchemaProbeDispatchResponse {
|
||||
return SCUMSchemaProbeDispatchResponse{ProbeRequest: SCUMSchemaProbeRequestFromDomain(request), QueuedJob: RemoteAdapterFromDomain(queued)}
|
||||
}
|
||||
|
||||
func SCUMSchemaProbeResultToDomain(value SCUMSchemaProbeResultDTO) domain.SCUMSchemaProbeResult {
|
||||
objects := make([]domain.SCUMSchemaObjectEvidence, len(value.Objects))
|
||||
for index, object := range value.Objects {
|
||||
columns := make([]domain.SCUMSchemaColumnEvidence, len(object.DeclaredColumns))
|
||||
for i, column := range object.DeclaredColumns {
|
||||
columns[i] = domain.SCUMSchemaColumnEvidence{NameFingerprint: column.NameFingerprint, DeclaredType: column.DeclaredType, Nullable: column.Nullable, PrimaryKey: column.PrimaryKey, Ordinal: column.Ordinal}
|
||||
}
|
||||
indexes := make([]domain.SCUMSchemaIndexEvidence, len(object.Indexes))
|
||||
for i, item := range object.Indexes {
|
||||
indexes[i] = domain.SCUMSchemaIndexEvidence{NameFingerprint: item.NameFingerprint, Unique: item.Unique, ColumnHashes: append([]string(nil), item.ColumnHashes...)}
|
||||
}
|
||||
foreignKeys := make([]domain.SCUMSchemaForeignKeyEvidence, len(object.ForeignKeys))
|
||||
for i, item := range object.ForeignKeys {
|
||||
foreignKeys[i] = domain.SCUMSchemaForeignKeyEvidence{FromColumnHash: item.FromColumnHash, ToObjectHash: item.ToObjectHash, ToColumnHash: item.ToColumnHash}
|
||||
}
|
||||
objects[index] = domain.SCUMSchemaObjectEvidence{ObjectHash: object.ObjectHash, Kind: object.Kind, NameFingerprint: object.NameFingerprint, DeclaredColumns: columns, Indexes: indexes, ForeignKeys: foreignKeys, ApproximateRows: object.ApproximateRows, SampleFingerprints: append([]string(nil), object.SampleFingerprints...)}
|
||||
}
|
||||
return domain.SCUMSchemaProbeResult{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Status: domain.SCUMCapabilityEvidenceStatus(value.Status), SourceFingerprint: value.SourceFingerprint, SchemaFingerprint: value.SchemaFingerprint, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, Objects: objects, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorCode(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumProbeBoundsToDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMSchemaProbeResultPtrToDomain(value *SCUMSchemaProbeResultDTO) *domain.SCUMSchemaProbeResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMSchemaProbeResultToDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMSQLiteTemplateResultToDomain(value SCUMSQLiteTemplateResultDTO) domain.SCUMSQLiteTemplateResult {
|
||||
return domain.SCUMSQLiteTemplateResult{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Status: domain.SCUMTerminalResultStatus(value.Status), Capability: domain.SCUMDataCapability(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, SchemaFingerprint: value.SchemaFingerprint, AssetDigest: value.AssetDigest, ParameterDigest: value.ParameterDigest, SourceFingerprint: value.SourceFingerprint, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, RowCount: value.RowCount, Rows: domain.CopySCUMRows(value.Rows), Truncated: value.Truncated, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorCode(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumSQLiteTemplateBoundsToDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMSQLiteTemplateResultPtrToDomain(value *SCUMSQLiteTemplateResultDTO) *domain.SCUMSQLiteTemplateResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMSQLiteTemplateResultToDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMTypedRCONTemplateResultToDomain(value SCUMTypedRCONTemplateResultDTO) domain.SCUMTypedRCONTemplateResult {
|
||||
return domain.SCUMTypedRCONTemplateResult{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Status: domain.SCUMTerminalResultStatus(value.Status), Capability: domain.SCUMDataCapability(value.Capability), TransportKey: value.TransportKey, TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, SchemaFingerprint: value.SchemaFingerprint, AssetDigest: value.AssetDigest, PayloadDigest: value.PayloadDigest, ConfirmationDigest: value.ConfirmationDigest, TargetIdentityDigest: value.TargetIdentityDigest, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, ResponseDigest: value.ResponseDigest, ConfirmationStatus: domain.SCUMRCONConfirmationStatus(value.ConfirmationStatus), ConfirmationDigestID: value.ConfirmationDigestID, SafeSummary: value.SafeSummary, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorCode(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumTypedRCONTemplateBoundsToDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMTypedRCONTemplateResultPtrToDomain(value *SCUMTypedRCONTemplateResultDTO) *domain.SCUMTypedRCONTemplateResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMTypedRCONTemplateResultToDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationResultToDomain(value SCUMGuardedMutationResultDTO) domain.SCUMGuardedMutationResult {
|
||||
return domain.SCUMGuardedMutationResult{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Status: domain.SCUMTerminalResultStatus(value.Status), Capability: domain.SCUMDataCapability(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, SchemaFingerprint: value.SchemaFingerprint, AssetDigest: value.AssetDigest, SourceFingerprint: value.SourceFingerprint, TargetIdentityDigest: value.TargetIdentityDigest, ExpectedRowDigest: value.ExpectedRowDigest, ExpectedValueDigest: value.ExpectedValueDigest, ExpectedXMLDigest: value.ExpectedXMLDigest, PatchDigest: value.PatchDigest, BackupEvidenceDigest: value.BackupEvidenceDigest, OfflineEvidenceDigest: value.OfflineEvidenceDigest, DangerConfirmationDigest: value.DangerConfirmationDigest, ReadbackExpectationDigest: value.ReadbackExpectationDigest, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, BeforeDigest: value.BeforeDigest, AfterDigest: value.AfterDigest, ReadbackDigest: value.ReadbackDigest, AffectedRows: value.AffectedRows, ReadbackStatus: domain.SCUMMutationReadbackStatus(value.ReadbackStatus), SafeSummary: value.SafeSummary, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorCode(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumGuardedMutationBoundsToDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationResultPtrToDomain(value *SCUMGuardedMutationResultDTO) *domain.SCUMGuardedMutationResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMGuardedMutationResultToDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMParsedLogBatchResultToDomain(value SCUMParsedLogBatchResultDTO) domain.SCUMParsedLogBatchResult {
|
||||
events := make([]domain.SCUMParsedLogEvent, len(value.Events))
|
||||
for index, event := range value.Events {
|
||||
events[index] = domain.SCUMParsedLogEvent{EventType: event.EventType, OccurredAt: event.OccurredAt, Cursor: scumParsedLogCursorToDomain(event.Cursor), LogicalEventDigest: event.LogicalEventDigest, EventDigest: event.EventDigest, PayloadDigest: event.PayloadDigest, Payload: domain.CopySCUMValueMap(event.Payload)}
|
||||
}
|
||||
return domain.SCUMParsedLogBatchResult{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Status: domain.SCUMTerminalResultStatus(value.Status), SourceKey: value.SourceKey, StreamKey: value.StreamKey, ParserKey: value.ParserKey, ParserVersion: value.ParserVersion, AdapterVersion: value.AdapterVersion, AssetDigest: value.AssetDigest, ParserDigest: value.ParserDigest, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, FirstCursor: scumParsedLogCursorToDomain(value.FirstCursor), LastCursor: scumParsedLogCursorToDomain(value.LastCursor), TailState: domain.SCUMLogTailState(value.TailState), PartialLineBuffered: value.PartialLineBuffered, Replay: value.Replay, EventCount: value.EventCount, Events: events, SafeSummary: value.SafeSummary, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorCode(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumParsedLogBatchBoundsToDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMParsedLogBatchResultPtrToDomain(value *SCUMParsedLogBatchResultDTO) *domain.SCUMParsedLogBatchResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMParsedLogBatchResultToDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMSchemaProbeResultFromDomain(value domain.SCUMSchemaProbeResult) SCUMSchemaProbeResultDTO {
|
||||
value = domain.CopySCUMSchemaProbeResult(value)
|
||||
objects := make([]SCUMSchemaObjectEvidenceDTO, len(value.Objects))
|
||||
for index, object := range value.Objects {
|
||||
columns := make([]SCUMSchemaColumnEvidenceDTO, len(object.DeclaredColumns))
|
||||
for i, column := range object.DeclaredColumns {
|
||||
columns[i] = SCUMSchemaColumnEvidenceDTO{NameFingerprint: column.NameFingerprint, DeclaredType: column.DeclaredType, Nullable: column.Nullable, PrimaryKey: column.PrimaryKey, Ordinal: column.Ordinal}
|
||||
}
|
||||
indexes := make([]SCUMSchemaIndexEvidenceDTO, len(object.Indexes))
|
||||
for i, item := range object.Indexes {
|
||||
indexes[i] = SCUMSchemaIndexEvidenceDTO{NameFingerprint: item.NameFingerprint, Unique: item.Unique, ColumnHashes: append([]string(nil), item.ColumnHashes...)}
|
||||
}
|
||||
foreignKeys := make([]SCUMSchemaForeignKeyEvidenceDTO, len(object.ForeignKeys))
|
||||
for i, item := range object.ForeignKeys {
|
||||
foreignKeys[i] = SCUMSchemaForeignKeyEvidenceDTO{FromColumnHash: item.FromColumnHash, ToObjectHash: item.ToObjectHash, ToColumnHash: item.ToColumnHash}
|
||||
}
|
||||
objects[index] = SCUMSchemaObjectEvidenceDTO{ObjectHash: object.ObjectHash, Kind: object.Kind, NameFingerprint: object.NameFingerprint, DeclaredColumns: columns, Indexes: indexes, ForeignKeys: foreignKeys, ApproximateRows: object.ApproximateRows, SampleFingerprints: append([]string(nil), object.SampleFingerprints...)}
|
||||
}
|
||||
return SCUMSchemaProbeResultDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Status: string(value.Status), SourceFingerprint: value.SourceFingerprint, SchemaFingerprint: value.SchemaFingerprint, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, Objects: objects, SafeError: SCUMSafeErrorDTO{Code: string(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumProbeBoundsFromDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMSchemaProbeResultPtrFromDomain(value *domain.SCUMSchemaProbeResult) *SCUMSchemaProbeResultDTO {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMSchemaProbeResultFromDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMSQLiteTemplateResultFromDomain(value domain.SCUMSQLiteTemplateResult) SCUMSQLiteTemplateResultDTO {
|
||||
value = *domain.CopySCUMSQLiteTemplateResultPtr(&value)
|
||||
return SCUMSQLiteTemplateResultDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Status: string(value.Status), Capability: string(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, SchemaFingerprint: value.SchemaFingerprint, AssetDigest: value.AssetDigest, ParameterDigest: value.ParameterDigest, SourceFingerprint: value.SourceFingerprint, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, RowCount: value.RowCount, Rows: value.Rows, Truncated: value.Truncated, SafeError: SCUMSafeErrorDTO{Code: string(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumSQLiteTemplateBoundsFromDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMSQLiteTemplateResultPtrFromDomain(value *domain.SCUMSQLiteTemplateResult) *SCUMSQLiteTemplateResultDTO {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMSQLiteTemplateResultFromDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMTypedRCONTemplateResultFromDomain(value domain.SCUMTypedRCONTemplateResult) SCUMTypedRCONTemplateResultDTO {
|
||||
return SCUMTypedRCONTemplateResultDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Status: string(value.Status), Capability: string(value.Capability), TransportKey: value.TransportKey, TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, SchemaFingerprint: value.SchemaFingerprint, AssetDigest: value.AssetDigest, PayloadDigest: value.PayloadDigest, ConfirmationDigest: value.ConfirmationDigest, TargetIdentityDigest: value.TargetIdentityDigest, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, ResponseDigest: value.ResponseDigest, ConfirmationStatus: string(value.ConfirmationStatus), ConfirmationDigestID: value.ConfirmationDigestID, SafeSummary: value.SafeSummary, SafeError: SCUMSafeErrorDTO{Code: string(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumTypedRCONTemplateBoundsFromDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMTypedRCONTemplateResultPtrFromDomain(value *domain.SCUMTypedRCONTemplateResult) *SCUMTypedRCONTemplateResultDTO {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMTypedRCONTemplateResultFromDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationResultFromDomain(value domain.SCUMGuardedMutationResult) SCUMGuardedMutationResultDTO {
|
||||
return SCUMGuardedMutationResultDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Status: string(value.Status), Capability: string(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, SchemaFingerprint: value.SchemaFingerprint, AssetDigest: value.AssetDigest, SourceFingerprint: value.SourceFingerprint, TargetIdentityDigest: value.TargetIdentityDigest, ExpectedRowDigest: value.ExpectedRowDigest, ExpectedValueDigest: value.ExpectedValueDigest, ExpectedXMLDigest: value.ExpectedXMLDigest, PatchDigest: value.PatchDigest, BackupEvidenceDigest: value.BackupEvidenceDigest, OfflineEvidenceDigest: value.OfflineEvidenceDigest, DangerConfirmationDigest: value.DangerConfirmationDigest, ReadbackExpectationDigest: value.ReadbackExpectationDigest, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, BeforeDigest: value.BeforeDigest, AfterDigest: value.AfterDigest, ReadbackDigest: value.ReadbackDigest, AffectedRows: value.AffectedRows, ReadbackStatus: string(value.ReadbackStatus), SafeSummary: value.SafeSummary, SafeError: SCUMSafeErrorDTO{Code: string(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumGuardedMutationBoundsFromDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationResultPtrFromDomain(value *domain.SCUMGuardedMutationResult) *SCUMGuardedMutationResultDTO {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMGuardedMutationResultFromDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMParsedLogBatchResultFromDomain(value domain.SCUMParsedLogBatchResult) SCUMParsedLogBatchResultDTO {
|
||||
value = *domain.CopySCUMParsedLogBatchResultPtr(&value)
|
||||
events := make([]SCUMParsedLogEventDTO, len(value.Events))
|
||||
for index, event := range value.Events {
|
||||
events[index] = SCUMParsedLogEventDTO{EventType: event.EventType, OccurredAt: event.OccurredAt, Cursor: scumParsedLogCursorFromDomain(event.Cursor), LogicalEventDigest: event.LogicalEventDigest, EventDigest: event.EventDigest, PayloadDigest: event.PayloadDigest, Payload: event.Payload}
|
||||
}
|
||||
return SCUMParsedLogBatchResultDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Status: string(value.Status), SourceKey: value.SourceKey, StreamKey: value.StreamKey, ParserKey: value.ParserKey, ParserVersion: value.ParserVersion, AdapterVersion: value.AdapterVersion, AssetDigest: value.AssetDigest, ParserDigest: value.ParserDigest, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, FirstCursor: scumParsedLogCursorFromDomain(value.FirstCursor), LastCursor: scumParsedLogCursorFromDomain(value.LastCursor), TailState: string(value.TailState), PartialLineBuffered: value.PartialLineBuffered, Replay: value.Replay, EventCount: value.EventCount, Events: events, SafeSummary: value.SafeSummary, SafeError: SCUMSafeErrorDTO{Code: string(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumParsedLogBatchBoundsFromDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMParsedLogBatchResultPtrFromDomain(value *domain.SCUMParsedLogBatchResult) *SCUMParsedLogBatchResultDTO {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMParsedLogBatchResultFromDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMCapabilityGateFromDomain(value domain.SCUMCapabilityGate) SCUMCapabilityGateDTO {
|
||||
return SCUMCapabilityGateDTO{Capability: string(value.Capability), State: string(value.State), Enabled: value.Enabled, ReasonCode: string(value.ReasonCode), Reason: value.Reason}
|
||||
}
|
||||
|
||||
func SCUMCapabilityNegotiationFromDomain(value domain.SCUMCapabilityNegotiation) SCUMCapabilityNegotiationDTO {
|
||||
gates := make([]SCUMCapabilityGateDTO, len(value.Gates))
|
||||
for index, gate := range value.Gates {
|
||||
gates[index] = SCUMCapabilityGateFromDomain(gate)
|
||||
}
|
||||
return SCUMCapabilityNegotiationDTO{ServerInstanceID: value.ServerInstanceID, RunEndpointID: value.RunEndpointID, RunBindingID: value.RunBindingID, PluginID: value.PluginID, PluginVersion: value.PluginVersion, AdapterVersion: value.AdapterVersion, GameVersion: value.GameVersion, DatabaseIdentity: value.DatabaseIdentity, ProbeExecutorAvailable: value.ProbeExecutorAvailable, EvaluatedAt: value.EvaluatedAt, Gates: gates}
|
||||
}
|
||||
|
||||
func scumBindingIdentityToDomain(value SCUMBindingIdentityDTO) domain.SCUMBindingIdentity {
|
||||
return domain.SCUMBindingIdentity{ServerInstanceID: value.ServerInstanceID, RunBindingID: value.RunBindingID, RunEndpointID: value.RunEndpointID, PluginID: value.PluginID, PluginVersion: value.PluginVersion, AdapterVersion: value.AdapterVersion, GameVersion: value.GameVersion, DatabaseIdentity: value.DatabaseIdentity}
|
||||
}
|
||||
|
||||
func scumBindingIdentityFromDomain(value domain.SCUMBindingIdentity) SCUMBindingIdentityDTO {
|
||||
return SCUMBindingIdentityDTO{ServerInstanceID: value.ServerInstanceID, RunBindingID: value.RunBindingID, RunEndpointID: value.RunEndpointID, PluginID: value.PluginID, PluginVersion: value.PluginVersion, AdapterVersion: value.AdapterVersion, GameVersion: value.GameVersion, DatabaseIdentity: value.DatabaseIdentity}
|
||||
}
|
||||
|
||||
func scumProbeBoundsToDomain(value SCUMSchemaProbeBoundsDTO) domain.SCUMSchemaProbeBounds {
|
||||
return domain.SCUMSchemaProbeBounds{MaxObjects: value.MaxObjects, MaxColumnsPerObject: value.MaxColumnsPerObject, MaxIndexesPerObject: value.MaxIndexesPerObject, MaxForeignKeys: value.MaxForeignKeys, MaxCardinalityReads: value.MaxCardinalityReads, MaxSampleRows: value.MaxSampleRows, TimeoutMS: value.TimeoutMS, MaxResultBytes: value.MaxResultBytes}
|
||||
}
|
||||
|
||||
func scumProbeBoundsFromDomain(value domain.SCUMSchemaProbeBounds) SCUMSchemaProbeBoundsDTO {
|
||||
return SCUMSchemaProbeBoundsDTO{MaxObjects: value.MaxObjects, MaxColumnsPerObject: value.MaxColumnsPerObject, MaxIndexesPerObject: value.MaxIndexesPerObject, MaxForeignKeys: value.MaxForeignKeys, MaxCardinalityReads: value.MaxCardinalityReads, MaxSampleRows: value.MaxSampleRows, TimeoutMS: value.TimeoutMS, MaxResultBytes: value.MaxResultBytes}
|
||||
}
|
||||
|
||||
func scumSQLiteTemplateBoundsToDomain(value SCUMSQLiteTemplateBoundsDTO) domain.SCUMSQLiteTemplateBounds {
|
||||
return domain.SCUMSQLiteTemplateBounds{MaxParameters: value.MaxParameters, MaxRows: value.MaxRows, TimeoutMS: value.TimeoutMS, BusyTimeoutMS: value.BusyTimeoutMS, MaxResultBytes: value.MaxResultBytes}
|
||||
}
|
||||
|
||||
func scumSQLiteTemplateBoundsFromDomain(value domain.SCUMSQLiteTemplateBounds) SCUMSQLiteTemplateBoundsDTO {
|
||||
return SCUMSQLiteTemplateBoundsDTO{MaxParameters: value.MaxParameters, MaxRows: value.MaxRows, TimeoutMS: value.TimeoutMS, BusyTimeoutMS: value.BusyTimeoutMS, MaxResultBytes: value.MaxResultBytes}
|
||||
}
|
||||
|
||||
func scumTypedRCONTemplateBoundsToDomain(value SCUMTypedRCONTemplateBoundsDTO) domain.SCUMTypedRCONTemplateBounds {
|
||||
return domain.SCUMTypedRCONTemplateBounds{MaxPayloadBytes: value.MaxPayloadBytes, TimeoutMS: value.TimeoutMS, MaxResponseBytes: value.MaxResponseBytes, MaxConfirmRecords: value.MaxConfirmRecords}
|
||||
}
|
||||
|
||||
func scumTypedRCONTemplateBoundsFromDomain(value domain.SCUMTypedRCONTemplateBounds) SCUMTypedRCONTemplateBoundsDTO {
|
||||
return SCUMTypedRCONTemplateBoundsDTO{MaxPayloadBytes: value.MaxPayloadBytes, TimeoutMS: value.TimeoutMS, MaxResponseBytes: value.MaxResponseBytes, MaxConfirmRecords: value.MaxConfirmRecords}
|
||||
}
|
||||
|
||||
func scumGuardedMutationBoundsToDomain(value SCUMGuardedMutationBoundsDTO) domain.SCUMGuardedMutationBounds {
|
||||
return domain.SCUMGuardedMutationBounds{MaxPayloadBytes: value.MaxPayloadBytes, TimeoutMS: value.TimeoutMS, BusyTimeoutMS: value.BusyTimeoutMS, MaxReadbackBytes: value.MaxReadbackBytes, MaxAffectedRows: value.MaxAffectedRows}
|
||||
}
|
||||
|
||||
func scumGuardedMutationBoundsFromDomain(value domain.SCUMGuardedMutationBounds) SCUMGuardedMutationBoundsDTO {
|
||||
return SCUMGuardedMutationBoundsDTO{MaxPayloadBytes: value.MaxPayloadBytes, TimeoutMS: value.TimeoutMS, BusyTimeoutMS: value.BusyTimeoutMS, MaxReadbackBytes: value.MaxReadbackBytes, MaxAffectedRows: value.MaxAffectedRows}
|
||||
}
|
||||
|
||||
func scumParsedLogCursorToDomain(value SCUMParsedLogCursorDTO) domain.SCUMParsedLogCursor {
|
||||
return domain.SCUMParsedLogCursor{SourceIdentityDigest: value.SourceIdentityDigest, StreamGeneration: value.StreamGeneration, Sequence: value.Sequence}
|
||||
}
|
||||
|
||||
func scumParsedLogCursorFromDomain(value domain.SCUMParsedLogCursor) SCUMParsedLogCursorDTO {
|
||||
return SCUMParsedLogCursorDTO{SourceIdentityDigest: value.SourceIdentityDigest, StreamGeneration: value.StreamGeneration, Sequence: value.Sequence}
|
||||
}
|
||||
|
||||
func scumParsedLogBatchBoundsToDomain(value SCUMParsedLogBatchBoundsDTO) domain.SCUMParsedLogBatchBounds {
|
||||
return domain.SCUMParsedLogBatchBounds{MaxEvents: value.MaxEvents, MaxPayloadBytes: value.MaxPayloadBytes, MaxLineBytes: value.MaxLineBytes, MaxResultBytes: value.MaxResultBytes}
|
||||
}
|
||||
|
||||
func scumParsedLogBatchBoundsFromDomain(value domain.SCUMParsedLogBatchBounds) SCUMParsedLogBatchBoundsDTO {
|
||||
return SCUMParsedLogBatchBoundsDTO{MaxEvents: value.MaxEvents, MaxPayloadBytes: value.MaxPayloadBytes, MaxLineBytes: value.MaxLineBytes, MaxResultBytes: value.MaxResultBytes}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package dto
|
||||
|
||||
import "browser.local/platform/domain"
|
||||
|
||||
type SCUMPlayerLiveStateListResponse struct {
|
||||
Items []domain.SCUMPlayerLiveState `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type SCUMSquadListResponse struct {
|
||||
Items []domain.SCUMSquad `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type SCUMSquadMemberListResponse struct {
|
||||
Items []domain.SCUMSquadMember `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type SCUMVehicleListResponse struct {
|
||||
Items []domain.SCUMVehicle `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type SCUMFlagListResponse struct {
|
||||
Items []domain.SCUMFlag `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type SCUMCurrentPositionListResponse struct {
|
||||
Items []domain.SCUMCurrentPosition `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
func SCUMPlayerLiveStatesFromDomain(values []domain.SCUMPlayerLiveState) SCUMPlayerLiveStateListResponse {
|
||||
out := make([]domain.SCUMPlayerLiveState, len(values))
|
||||
for index, value := range values {
|
||||
out[index] = domain.CopySCUMPlayerLiveState(value)
|
||||
}
|
||||
return SCUMPlayerLiveStateListResponse{Items: out, Count: len(out)}
|
||||
}
|
||||
|
||||
func SCUMSquadsFromDomain(values []domain.SCUMSquad) SCUMSquadListResponse {
|
||||
out := make([]domain.SCUMSquad, len(values))
|
||||
for index, value := range values {
|
||||
out[index] = domain.CopySCUMSquad(value)
|
||||
}
|
||||
return SCUMSquadListResponse{Items: out, Count: len(out)}
|
||||
}
|
||||
|
||||
func SCUMSquadMembersFromDomain(values []domain.SCUMSquadMember) SCUMSquadMemberListResponse {
|
||||
out := make([]domain.SCUMSquadMember, len(values))
|
||||
for index, value := range values {
|
||||
out[index] = domain.CopySCUMSquadMember(value)
|
||||
}
|
||||
return SCUMSquadMemberListResponse{Items: out, Count: len(out)}
|
||||
}
|
||||
|
||||
func SCUMVehiclesFromDomain(values []domain.SCUMVehicle) SCUMVehicleListResponse {
|
||||
out := make([]domain.SCUMVehicle, len(values))
|
||||
for index, value := range values {
|
||||
out[index] = domain.CopySCUMVehicle(value)
|
||||
}
|
||||
return SCUMVehicleListResponse{Items: out, Count: len(out)}
|
||||
}
|
||||
|
||||
func SCUMFlagsFromDomain(values []domain.SCUMFlag) SCUMFlagListResponse {
|
||||
out := make([]domain.SCUMFlag, len(values))
|
||||
for index, value := range values {
|
||||
out[index] = domain.CopySCUMFlag(value)
|
||||
}
|
||||
return SCUMFlagListResponse{Items: out, Count: len(out)}
|
||||
}
|
||||
|
||||
func SCUMCurrentPositionsFromDomain(values []domain.SCUMCurrentPosition) SCUMCurrentPositionListResponse {
|
||||
out := make([]domain.SCUMCurrentPosition, len(values))
|
||||
for index, value := range values {
|
||||
out[index] = domain.CopySCUMCurrentPosition(value)
|
||||
}
|
||||
return SCUMCurrentPositionListResponse{Items: out, Count: len(out)}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
type SCUMSafeSummaryBody struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Details map[string]string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMDataObservationResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
Source string `json:"source"`
|
||||
QueryKey string `json:"queryKey,omitempty"`
|
||||
SubjectType string `json:"subjectType,omitempty"`
|
||||
SubjectID string `json:"subjectId,omitempty"`
|
||||
Sequence uint64 `json:"sequence"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
Status string `json:"status"`
|
||||
ErrorCode string `json:"errorCode,omitempty"`
|
||||
SafeSummary SCUMSafeSummaryBody `json:"safeSummary,omitempty"`
|
||||
ObservedAt time.Time `json:"observedAt"`
|
||||
ReceivedAt time.Time `json:"receivedAt"`
|
||||
}
|
||||
|
||||
type SCUMProjectionFreshnessBody struct {
|
||||
Status string `json:"status"`
|
||||
ObservationID string `json:"observationId,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
QueryKey string `json:"queryKey,omitempty"`
|
||||
Sequence uint64 `json:"sequence,omitempty"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
StaleReason string `json:"staleReason,omitempty"`
|
||||
ObservedAt time.Time `json:"observedAt,omitempty"`
|
||||
ReceivedAt time.Time `json:"receivedAt,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMMutationGuardBody struct {
|
||||
FieldKey string `json:"fieldKey,omitempty"`
|
||||
Before any `json:"before,omitempty"`
|
||||
After any `json:"after,omitempty"`
|
||||
MaxRowsAffected int `json:"maxRowsAffected,omitempty"`
|
||||
SafetyWindow string `json:"safetyWindow,omitempty"`
|
||||
BackupRef string `json:"backupRef,omitempty"`
|
||||
RequiresOfflinePlayer bool `json:"requiresOfflinePlayer,omitempty"`
|
||||
RequiresMaintenance bool `json:"requiresMaintenance,omitempty"`
|
||||
RequiresBackup bool `json:"requiresBackup,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMOperationConfirmationBody struct {
|
||||
Status string `json:"status,omitempty"`
|
||||
ObservationID string `json:"observationId,omitempty"`
|
||||
ConfirmedFields map[string]any `json:"confirmedFields,omitempty"`
|
||||
AffectedRows int `json:"affectedRows,omitempty"`
|
||||
MutationChecksum string `json:"mutationChecksum,omitempty"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
ObservedAt time.Time `json:"observedAt,omitempty"`
|
||||
SafeSummary SCUMSafeSummaryBody `json:"safeSummary,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMOperationRequestBody struct {
|
||||
TemplateKey string `json:"templateKey"`
|
||||
PlayerID string `json:"playerId,omitempty"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
Guard SCUMMutationGuardBody `json:"guard,omitempty"`
|
||||
Reason string `json:"reason"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
}
|
||||
|
||||
type SCUMWorkflowCreateRequest struct {
|
||||
TemplateKey string `json:"templateKey"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
Input map[string]any `json:"input,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMOperationResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
PlayerID string `json:"playerId,omitempty"`
|
||||
RequesterID string `json:"requesterId,omitempty"`
|
||||
ApproverID string `json:"approverId,omitempty"`
|
||||
ApprovalLevel string `json:"approvalLevel"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
Guard SCUMMutationGuardBody `json:"guard,omitempty"`
|
||||
Confirmation SCUMOperationConfirmationBody `json:"confirmation,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
RunJobID string `json:"runJobId,omitempty"`
|
||||
SafeSummary SCUMSafeSummaryBody `json:"safeSummary,omitempty"`
|
||||
AuditReferences []string `json:"auditReferences,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
ApprovedAt time.Time `json:"approvedAt,omitempty"`
|
||||
CompletedAt time.Time `json:"completedAt,omitempty"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type SCUMOperationListResponse struct {
|
||||
Items []SCUMOperationResponse `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type SCUMWorkflowResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
RequestedBy string `json:"requestedBy,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
||||
Status string `json:"status"`
|
||||
CurrentStepKey string `json:"currentStepKey,omitempty"`
|
||||
Input map[string]any `json:"input,omitempty"`
|
||||
SafeSummary SCUMSafeSummaryBody `json:"safeSummary,omitempty"`
|
||||
BlockerReason string `json:"blockerReason,omitempty"`
|
||||
AuditReferences []string `json:"auditReferences,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
CompletedAt time.Time `json:"completedAt,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMWorkflowListResponse struct {
|
||||
Items []SCUMWorkflowResponse `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type SCUMWorkflowStepResponse struct {
|
||||
ID string `json:"id"`
|
||||
WorkflowID string `json:"workflowId"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
StepKey string `json:"stepKey"`
|
||||
DependsOn []string `json:"dependsOn,omitempty"`
|
||||
Status string `json:"status"`
|
||||
OperationKey string `json:"operationKey,omitempty"`
|
||||
QueryTemplateKey string `json:"queryTemplateKey,omitempty"`
|
||||
Capability string `json:"capability,omitempty"`
|
||||
TargetKey string `json:"targetKey,omitempty"`
|
||||
JobID string `json:"jobId,omitempty"`
|
||||
Attempt int `json:"attempt,omitempty"`
|
||||
MaxAttempts int `json:"maxAttempts,omitempty"`
|
||||
MutatesState bool `json:"mutatesState,omitempty"`
|
||||
Confirmation SCUMOperationConfirmationBody `json:"confirmation,omitempty"`
|
||||
SafeSummary SCUMSafeSummaryBody `json:"safeSummary,omitempty"`
|
||||
BlockerReason string `json:"blockerReason,omitempty"`
|
||||
AuditReferences []string `json:"auditReferences,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
CompletedAt time.Time `json:"completedAt,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMWorkflowStepListResponse struct {
|
||||
Items []SCUMWorkflowStepResponse `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
func SCUMSafeSummaryFromDomain(value domain.SCUMSafeSummary) SCUMSafeSummaryBody {
|
||||
value = domain.CopySCUMSafeSummary(value)
|
||||
return SCUMSafeSummaryBody{Title: value.Title, Message: value.Message, Details: value.Details}
|
||||
}
|
||||
|
||||
func scumSafeSummaryToDomain(value SCUMSafeSummaryBody) domain.SCUMSafeSummary {
|
||||
return domain.SCUMSafeSummary{Title: value.Title, Message: value.Message, Details: domain.CopyStringMap(value.Details)}
|
||||
}
|
||||
|
||||
func SCUMDataObservationFromDomain(value domain.SCUMDataObservation) SCUMDataObservationResponse {
|
||||
value = domain.CopySCUMDataObservation(value)
|
||||
return SCUMDataObservationResponse{ID: value.ID, ServerInstanceID: value.ServerInstanceID, PluginID: value.PluginID, Source: value.Source, QueryKey: value.QueryKey, SubjectType: value.SubjectType, SubjectID: value.SubjectID, Sequence: value.Sequence, Checksum: value.Checksum, Status: string(value.Status), ErrorCode: value.ErrorCode, SafeSummary: SCUMSafeSummaryFromDomain(value.SafeSummary), ObservedAt: value.ObservedAt, ReceivedAt: value.ReceivedAt}
|
||||
}
|
||||
|
||||
func SCUMProjectionFreshnessFromDomain(value domain.SCUMProjectionFreshnessState) SCUMProjectionFreshnessBody {
|
||||
value = domain.CopySCUMProjectionFreshnessState(value)
|
||||
return SCUMProjectionFreshnessBody{Status: string(value.Status), ObservationID: value.ObservationID, Source: value.Source, QueryKey: value.QueryKey, Sequence: value.Sequence, Checksum: value.Checksum, StaleReason: value.StaleReason, ObservedAt: value.ObservedAt, ReceivedAt: value.ReceivedAt}
|
||||
}
|
||||
|
||||
func SCUMOperationRequestBodyToDomain(request SCUMOperationRequestBody) domain.SCUMOperationRequest {
|
||||
return domain.SCUMOperationRequest{TemplateKey: request.TemplateKey, PlayerID: request.PlayerID, Payload: domain.CopyGameClientBridgePayload(request.Payload), Guard: scumMutationGuardToDomain(request.Guard), Reason: request.Reason, IdempotencyKey: request.IdempotencyKey}
|
||||
}
|
||||
|
||||
func SCUMWorkflowCreateRequestToDomain(request SCUMWorkflowCreateRequest) domain.SCUMWorkflowInstance {
|
||||
return domain.SCUMWorkflowInstance{TemplateKey: request.TemplateKey, IdempotencyKey: request.IdempotencyKey, Input: domain.CopyGameClientBridgePayload(request.Input)}
|
||||
}
|
||||
|
||||
func SCUMOperationFromDomain(value domain.SCUMOperationRequest) SCUMOperationResponse {
|
||||
value = domain.CopySCUMOperationRequest(value)
|
||||
return SCUMOperationResponse{ID: value.ID, ServerInstanceID: value.ServerInstanceID, PluginID: value.PluginID, TemplateKey: value.TemplateKey, PlayerID: value.PlayerID, RequesterID: value.RequesterID, ApproverID: value.ApproverID, ApprovalLevel: string(value.ApprovalLevel), Payload: value.Payload, Guard: scumMutationGuardFromDomain(value.Guard), Confirmation: scumOperationConfirmationFromDomain(value.Confirmation), Status: string(value.Status), Reason: value.Reason, RunJobID: value.RunJobID, SafeSummary: SCUMSafeSummaryFromDomain(value.SafeSummary), AuditReferences: value.AuditReferences, CreatedAt: value.CreatedAt, ApprovedAt: value.ApprovedAt, CompletedAt: value.CompletedAt, UpdatedAt: value.UpdatedAt}
|
||||
}
|
||||
|
||||
func SCUMOperationsFromDomain(values []domain.SCUMOperationRequest) SCUMOperationListResponse {
|
||||
items := make([]SCUMOperationResponse, len(values))
|
||||
for index, value := range values {
|
||||
items[index] = SCUMOperationFromDomain(value)
|
||||
}
|
||||
return SCUMOperationListResponse{Items: items, Count: len(items)}
|
||||
}
|
||||
|
||||
func SCUMWorkflowFromDomain(value domain.SCUMWorkflowInstance) SCUMWorkflowResponse {
|
||||
value = domain.CopySCUMWorkflowInstance(value)
|
||||
return SCUMWorkflowResponse{ID: value.ID, ServerInstanceID: value.ServerInstanceID, PluginID: value.PluginID, TemplateKey: value.TemplateKey, RequestedBy: value.RequestedBy, IdempotencyKey: value.IdempotencyKey, Status: string(value.Status), CurrentStepKey: value.CurrentStepKey, Input: value.Input, SafeSummary: SCUMSafeSummaryFromDomain(value.SafeSummary), BlockerReason: value.BlockerReason, AuditReferences: value.AuditReferences, CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt, CompletedAt: value.CompletedAt}
|
||||
}
|
||||
|
||||
func SCUMWorkflowsFromDomain(values []domain.SCUMWorkflowInstance) SCUMWorkflowListResponse {
|
||||
items := make([]SCUMWorkflowResponse, len(values))
|
||||
for index, value := range values {
|
||||
items[index] = SCUMWorkflowFromDomain(value)
|
||||
}
|
||||
return SCUMWorkflowListResponse{Items: items, Count: len(items)}
|
||||
}
|
||||
|
||||
func SCUMWorkflowStepFromDomain(value domain.SCUMWorkflowStep) SCUMWorkflowStepResponse {
|
||||
value = domain.CopySCUMWorkflowStep(value)
|
||||
return SCUMWorkflowStepResponse{ID: value.ID, WorkflowID: value.WorkflowID, ServerInstanceID: value.ServerInstanceID, StepKey: value.StepKey, DependsOn: value.DependsOn, Status: string(value.Status), OperationKey: value.OperationKey, QueryTemplateKey: value.QueryTemplateKey, Capability: value.Capability, TargetKey: value.TargetKey, JobID: value.JobID, Attempt: value.Attempt, MaxAttempts: value.MaxAttempts, MutatesState: value.MutatesState, Confirmation: scumOperationConfirmationFromDomain(value.Confirmation), SafeSummary: SCUMSafeSummaryFromDomain(value.SafeSummary), BlockerReason: value.BlockerReason, AuditReferences: value.AuditReferences, CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt, CompletedAt: value.CompletedAt}
|
||||
}
|
||||
|
||||
func SCUMWorkflowStepsFromDomain(values []domain.SCUMWorkflowStep) SCUMWorkflowStepListResponse {
|
||||
items := make([]SCUMWorkflowStepResponse, len(values))
|
||||
for index, value := range values {
|
||||
items[index] = SCUMWorkflowStepFromDomain(value)
|
||||
}
|
||||
return SCUMWorkflowStepListResponse{Items: items, Count: len(items)}
|
||||
}
|
||||
|
||||
func scumMutationGuardFromDomain(value domain.SCUMMutationGuard) SCUMMutationGuardBody {
|
||||
return SCUMMutationGuardBody{FieldKey: value.FieldKey, Before: value.Before, After: value.After, MaxRowsAffected: value.MaxRowsAffected, SafetyWindow: value.SafetyWindow, BackupRef: value.BackupRef, RequiresOfflinePlayer: value.RequiresOfflinePlayer, RequiresMaintenance: value.RequiresMaintenance, RequiresBackup: value.RequiresBackup}
|
||||
}
|
||||
|
||||
func scumMutationGuardToDomain(value SCUMMutationGuardBody) domain.SCUMMutationGuard {
|
||||
return domain.SCUMMutationGuard{FieldKey: value.FieldKey, Before: value.Before, After: value.After, MaxRowsAffected: value.MaxRowsAffected, SafetyWindow: value.SafetyWindow, BackupRef: value.BackupRef, RequiresOfflinePlayer: value.RequiresOfflinePlayer, RequiresMaintenance: value.RequiresMaintenance, RequiresBackup: value.RequiresBackup}
|
||||
}
|
||||
|
||||
func scumOperationConfirmationFromDomain(value domain.SCUMOperationConfirmation) SCUMOperationConfirmationBody {
|
||||
value = domain.CopySCUMOperationConfirmation(value)
|
||||
return SCUMOperationConfirmationBody{Status: value.Status, ObservationID: value.ObservationID, ConfirmedFields: value.ConfirmedFields, AffectedRows: value.AffectedRows, MutationChecksum: value.MutationChecksum, Checksum: value.Checksum, ObservedAt: value.ObservedAt, SafeSummary: SCUMSafeSummaryFromDomain(value.SafeSummary)}
|
||||
}
|
||||
|
||||
func scumOperationConfirmationToDomain(value SCUMOperationConfirmationBody) domain.SCUMOperationConfirmation {
|
||||
return domain.SCUMOperationConfirmation{Status: value.Status, ObservationID: value.ObservationID, ConfirmedFields: domain.CopyGameClientBridgePayload(value.ConfirmedFields), AffectedRows: value.AffectedRows, MutationChecksum: value.MutationChecksum, Checksum: value.Checksum, ObservedAt: value.ObservedAt, SafeSummary: scumSafeSummaryToDomain(value.SafeSummary)}
|
||||
}
|
||||
+13
-16
@@ -309,21 +309,18 @@ type JobExecutionInput struct {
|
||||
Deployment *domain.ServerDeploymentDefinition `json:"deployment,omitempty" db:"deployment"`
|
||||
// ServerDeploymentPlan is the legacy generic deployment-plan payload.
|
||||
ServerDeploymentPlan *domain.ServerDeploymentPlan `json:"serverDeploymentPlan,omitempty" db:"server_deployment_plan"`
|
||||
// SQLiteSchemaProbe is the typed, bounded diagnostic request delivered only to fenced Run assignments.
|
||||
SQLiteSchemaProbe *domain.SCUMSchemaProbeRequest `json:"sqliteSchemaProbe,omitempty" db:"sqlite_schema_probe"`
|
||||
}
|
||||
|
||||
type JobExecutionResult struct {
|
||||
Kind string `json:"kind,omitempty" db:"kind"`
|
||||
ProcessState string `json:"processState,omitempty" db:"process_state"`
|
||||
ExitClassification string `json:"exitClassification,omitempty" db:"exit_classification"`
|
||||
ExitCode int `json:"exitCode,omitempty" db:"exit_code"`
|
||||
Version int `json:"version,omitempty" db:"version"`
|
||||
Checksum string `json:"checksum,omitempty" db:"checksum"`
|
||||
SizeBytes int64 `json:"sizeBytes,omitempty" db:"size_bytes"`
|
||||
AuditSummary string `json:"auditSummary,omitempty" db:"audit_summary"`
|
||||
Content string `json:"content,omitempty" db:"content"`
|
||||
SQLiteSchemaProbe *domain.SCUMSchemaProbeResult `json:"sqliteSchemaProbe,omitempty" db:"sqlite_schema_probe"`
|
||||
Kind string `json:"kind,omitempty" db:"kind"`
|
||||
ProcessState string `json:"processState,omitempty" db:"process_state"`
|
||||
ExitClassification string `json:"exitClassification,omitempty" db:"exit_classification"`
|
||||
ExitCode int `json:"exitCode,omitempty" db:"exit_code"`
|
||||
Version int `json:"version,omitempty" db:"version"`
|
||||
Checksum string `json:"checksum,omitempty" db:"checksum"`
|
||||
SizeBytes int64 `json:"sizeBytes,omitempty" db:"size_bytes"`
|
||||
AuditSummary string `json:"auditSummary,omitempty" db:"audit_summary"`
|
||||
Content string `json:"content,omitempty" db:"content"`
|
||||
}
|
||||
|
||||
type Job struct {
|
||||
@@ -916,7 +913,7 @@ func executionInputFromDomain(input domain.JobExecutionInput) JobExecutionInput
|
||||
copy := domain.CopyServerDeploymentDefinition(*input.Deployment)
|
||||
deployment = ©
|
||||
}
|
||||
return JobExecutionInput{WorkspaceScope: input.WorkspaceScope, Content: input.Content, ExpectedVersion: input.ExpectedVersion, ExpectedChecksum: input.ExpectedChecksum, MaxReadBytes: input.MaxReadBytes, RemoteAdapterKey: input.RemoteAdapterKey, RemoteAdapterKind: input.RemoteAdapterKind, TimeoutSeconds: input.TimeoutSeconds, PluginID: input.PluginID, LifecycleOperation: input.LifecycleOperation, TargetVersion: input.TargetVersion, Inputs: domain.CopyStringMap(input.Inputs), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), input.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(input.SourceRCON), Deployment: deployment, ServerDeploymentPlan: domain.CopyServerDeploymentPlan(input.ServerDeploymentPlan), SQLiteSchemaProbe: domain.CopySCUMSchemaProbeRequestPtr(input.SQLiteSchemaProbe)}
|
||||
return JobExecutionInput{WorkspaceScope: input.WorkspaceScope, Content: input.Content, ExpectedVersion: input.ExpectedVersion, ExpectedChecksum: input.ExpectedChecksum, MaxReadBytes: input.MaxReadBytes, RemoteAdapterKey: input.RemoteAdapterKey, RemoteAdapterKind: input.RemoteAdapterKind, TimeoutSeconds: input.TimeoutSeconds, PluginID: input.PluginID, LifecycleOperation: input.LifecycleOperation, TargetVersion: input.TargetVersion, Inputs: domain.CopyStringMap(input.Inputs), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), input.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(input.SourceRCON), Deployment: deployment, ServerDeploymentPlan: domain.CopyServerDeploymentPlan(input.ServerDeploymentPlan)}
|
||||
}
|
||||
|
||||
func (input JobExecutionInput) ToDomain() domain.JobExecutionInput {
|
||||
@@ -925,15 +922,15 @@ func (input JobExecutionInput) ToDomain() domain.JobExecutionInput {
|
||||
copy := domain.CopyServerDeploymentDefinition(*input.Deployment)
|
||||
deployment = ©
|
||||
}
|
||||
return domain.JobExecutionInput{WorkspaceScope: input.WorkspaceScope, Content: input.Content, ExpectedVersion: input.ExpectedVersion, ExpectedChecksum: input.ExpectedChecksum, MaxReadBytes: input.MaxReadBytes, RemoteAdapterKey: input.RemoteAdapterKey, RemoteAdapterKind: input.RemoteAdapterKind, TimeoutSeconds: input.TimeoutSeconds, PluginID: input.PluginID, LifecycleOperation: input.LifecycleOperation, TargetVersion: input.TargetVersion, Inputs: domain.CopyStringMap(input.Inputs), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), input.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(input.SourceRCON), Deployment: deployment, ServerDeploymentPlan: domain.CopyServerDeploymentPlan(input.ServerDeploymentPlan), SQLiteSchemaProbe: domain.CopySCUMSchemaProbeRequestPtr(input.SQLiteSchemaProbe)}
|
||||
return domain.JobExecutionInput{WorkspaceScope: input.WorkspaceScope, Content: input.Content, ExpectedVersion: input.ExpectedVersion, ExpectedChecksum: input.ExpectedChecksum, MaxReadBytes: input.MaxReadBytes, RemoteAdapterKey: input.RemoteAdapterKey, RemoteAdapterKind: input.RemoteAdapterKind, TimeoutSeconds: input.TimeoutSeconds, PluginID: input.PluginID, LifecycleOperation: input.LifecycleOperation, TargetVersion: input.TargetVersion, Inputs: domain.CopyStringMap(input.Inputs), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), input.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(input.SourceRCON), Deployment: deployment, ServerDeploymentPlan: domain.CopyServerDeploymentPlan(input.ServerDeploymentPlan)}
|
||||
}
|
||||
|
||||
func executionResultFromDomain(result domain.JobExecutionResult) JobExecutionResult {
|
||||
return JobExecutionResult{Kind: result.Kind, ProcessState: result.ProcessState, ExitClassification: result.ExitClassification, ExitCode: result.ExitCode, Version: result.Version, Checksum: result.Checksum, SizeBytes: result.SizeBytes, AuditSummary: result.AuditSummary, Content: result.Content, SQLiteSchemaProbe: domain.CopySCUMSchemaProbeResultPtr(result.SQLiteSchemaProbe)}
|
||||
return JobExecutionResult{Kind: result.Kind, ProcessState: result.ProcessState, ExitClassification: result.ExitClassification, ExitCode: result.ExitCode, Version: result.Version, Checksum: result.Checksum, SizeBytes: result.SizeBytes, AuditSummary: result.AuditSummary, Content: result.Content}
|
||||
}
|
||||
|
||||
func (result JobExecutionResult) ToDomain() domain.JobExecutionResult {
|
||||
return domain.JobExecutionResult{Kind: result.Kind, ProcessState: result.ProcessState, ExitClassification: result.ExitClassification, ExitCode: result.ExitCode, Version: result.Version, Checksum: result.Checksum, SizeBytes: result.SizeBytes, AuditSummary: result.AuditSummary, Content: result.Content, SQLiteSchemaProbe: domain.CopySCUMSchemaProbeResultPtr(result.SQLiteSchemaProbe)}
|
||||
return domain.JobExecutionResult{Kind: result.Kind, ProcessState: result.ProcessState, ExitClassification: result.ExitClassification, ExitCode: result.ExitCode, Version: result.Version, Checksum: result.Checksum, SizeBytes: result.SizeBytes, AuditSummary: result.AuditSummary, Content: result.Content}
|
||||
}
|
||||
|
||||
func (policy JobRetryPolicy) ToDomain() domain.JobRetryPolicy {
|
||||
|
||||
@@ -119,12 +119,6 @@ func TestJobExecutionInputModelRoundTripPreservesLifecycleMetadata(t *testing.T)
|
||||
PluginID: "game.scum",
|
||||
Prerequisites: []domain.RuntimeServerPrerequisite{{Key: "steamcmd", Kind: "tool"}},
|
||||
},
|
||||
SQLiteSchemaProbe: &domain.SCUMSchemaProbeRequest{
|
||||
RequestID: "probe-1",
|
||||
JobID: "job-1",
|
||||
Binding: domain.SCUMBindingIdentity{ServerInstanceID: "server-1", RunBindingID: "runtime-binding-1", RunEndpointID: "run-1", PluginID: "game.scum", PluginVersion: "0.1.6", AdapterVersion: "adapter-1", DatabaseIdentity: "scum-database"},
|
||||
Bounds: domain.DefaultSCUMSchemaProbeBounds(),
|
||||
},
|
||||
}
|
||||
|
||||
row := executionInputFromDomain(source)
|
||||
@@ -136,13 +130,8 @@ func TestJobExecutionInputModelRoundTripPreservesLifecycleMetadata(t *testing.T)
|
||||
if row.Deployment == nil || row.Deployment.CreateInputs["maxPlayers"] != "128" {
|
||||
t.Fatalf("expected model deployment inputs to be isolated from source mutation, row=%+v", row.Deployment)
|
||||
}
|
||||
source.SQLiteSchemaProbe.Bounds.MaxObjects = 1
|
||||
if row.SQLiteSchemaProbe == nil || row.SQLiteSchemaProbe.Bounds.MaxObjects != domain.DefaultSCUMSchemaProbeBounds().MaxObjects {
|
||||
t.Fatalf("expected model schema probe to be isolated from source mutation, row=%+v", row.SQLiteSchemaProbe)
|
||||
}
|
||||
source.Inputs["playerId"] = "steam-123"
|
||||
source.Deployment.CreateInputs["maxPlayers"] = "128"
|
||||
source.SQLiteSchemaProbe.Bounds.MaxObjects = domain.DefaultSCUMSchemaProbeBounds().MaxObjects
|
||||
row.Inputs["playerId"] = "row-mutated"
|
||||
if source.Inputs["playerId"] != "steam-123" {
|
||||
t.Fatalf("expected source execution inputs to be isolated from model mutation, source=%+v", source.Inputs)
|
||||
@@ -151,13 +140,8 @@ func TestJobExecutionInputModelRoundTripPreservesLifecycleMetadata(t *testing.T)
|
||||
if source.Deployment.CreateInputs["maxPlayers"] != "128" {
|
||||
t.Fatalf("expected source deployment inputs to be isolated from model mutation, source=%+v", source.Deployment)
|
||||
}
|
||||
row.SQLiteSchemaProbe.Bounds.MaxObjects = 2
|
||||
if source.SQLiteSchemaProbe.Bounds.MaxObjects != domain.DefaultSCUMSchemaProbeBounds().MaxObjects {
|
||||
t.Fatalf("expected source schema probe to be isolated from model mutation, source=%+v", source.SQLiteSchemaProbe)
|
||||
}
|
||||
row.Inputs["playerId"] = "steam-123"
|
||||
row.Deployment.CreateInputs["maxPlayers"] = "128"
|
||||
row.SQLiteSchemaProbe.Bounds.MaxObjects = domain.DefaultSCUMSchemaProbeBounds().MaxObjects
|
||||
|
||||
roundTrip := row.ToDomain()
|
||||
if !reflect.DeepEqual(roundTrip, source) {
|
||||
@@ -165,8 +149,7 @@ func TestJobExecutionInputModelRoundTripPreservesLifecycleMetadata(t *testing.T)
|
||||
}
|
||||
roundTrip.Inputs["playerId"] = "mutated"
|
||||
roundTrip.Deployment.CreateInputs["maxPlayers"] = "16"
|
||||
roundTrip.SQLiteSchemaProbe.Bounds.MaxObjects = 3
|
||||
if source.Inputs["playerId"] != "steam-123" || row.Inputs["playerId"] != "steam-123" || source.Deployment.CreateInputs["maxPlayers"] != "128" || row.Deployment.CreateInputs["maxPlayers"] != "128" || source.SQLiteSchemaProbe.Bounds.MaxObjects != domain.DefaultSCUMSchemaProbeBounds().MaxObjects || row.SQLiteSchemaProbe.Bounds.MaxObjects != domain.DefaultSCUMSchemaProbeBounds().MaxObjects {
|
||||
if source.Inputs["playerId"] != "steam-123" || row.Inputs["playerId"] != "steam-123" || source.Deployment.CreateInputs["maxPlayers"] != "128" || row.Deployment.CreateInputs["maxPlayers"] != "128" {
|
||||
t.Fatalf("expected execution inputs to round-trip without aliasing, source=%+v row=%+v", source, row)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,8 +86,6 @@ Named log DTOs:
|
||||
|
||||
Log ingest supports bounded batches, sequence ranges, checksum validation, retry-safe duplicate acknowledgement, latest sequence tracking, cursor query, and browser SSE fan-out from already-ingested platform logs. Log payloads must not carry artifact chunks, host paths, raw credentials, direct sockets, or unbounded inline data.
|
||||
|
||||
Plugin-declared parsed-log batches use the signed job-result channel for typed `log.parsed-events` terminal envelopes when a bounded backfill/replay job is leased, while ordinary log bodies continue to use `/run/logs/batches`. The parsed-log envelope carries only logical source/stream/parser identity, redacted source identity digest, stream generation, sequence cursor, logical event digest, payload digest, sanitized scalar payload, tail state, safe error, and applied limits. It must not carry raw log lines, paths, globs, network identifiers, sockets, credentials, SQL, XML, or game-specific executor branch data.
|
||||
|
||||
Run-assigned Platform jobs use `job.<jobId>.<streamKey>` log stream IDs. Autonomous lifecycle bootstrap is Run-owned machine execution rather than a Platform job, so its durable process logs use `run.<runEndpointId>.<serverInstanceId>.<streamKey>`. Platform may auto-create those streams only after validating the active Run session and the server-to-Run binding. For retry compatibility, legacy spooled `job.autonomous-*.<streamKey>` batches are accepted as Run-owned autonomous streams without creating or completing a Platform job.
|
||||
|
||||
Log ingest is durable and independently retried. Artifact/file transfer backlog must not prevent log batch acknowledgement, duplicate acknowledgement, cursor state updates, or spool cleanup.
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
# SCUM Live Data Contracts
|
||||
|
||||
This contract replaces SCUM projection/Workflow-facing reads with evidence-gated local management data. It is intentionally generic at the Run boundary: Platform and plugins may name SCUM capabilities, but Run receives only packaged generic SQLite probe/template/mutation work and never hardcodes SCUM table names, command keys, host paths, sockets, credentials, or browser-supplied SQL.
|
||||
|
||||
## Capability gate
|
||||
|
||||
Every database-backed SCUM read/write capability is disabled until all of the following are true for the active server binding:
|
||||
|
||||
- the bound Run advertises `remote.run.db.sqlite.probe`;
|
||||
- Platform has a current `SCUMCapabilityEvidence` row for the exact server instance, Run binding, Run endpoint, plugin id/version, adapter version, game version, and database identity;
|
||||
- evidence status is `compatible` for the requested capability;
|
||||
- the evidence schema fingerprint equals the versioned adapter requirement;
|
||||
- every required packaged asset digest is present in the evidence;
|
||||
- evidence has not expired or been invalidated by rebinding, database identity change, plugin version change, adapter version change, or schema fingerprint change.
|
||||
|
||||
If any condition fails, APIs and plugin pages receive a safe disabled state such as `probe_missing`, `probe_executor_absent`, `binding_mismatch`, `fingerprint_mismatch`, `digest_mismatch`, `schema_incompatible`, or `evidence_expired`. Disabled states are ordinary availability results, not projection/audit/workflow work items.
|
||||
|
||||
Platform evaluates the full capability set through a read-only negotiation step for the active server instance. The negotiation combines the current server/plugin/Run endpoint/runtime binding, the plugin's per-capability requirements, the bound Run capability list, and the latest accepted typed terminal evidence for the same binding. It returns independent gates for schema probe, read-only SQLite-backed player/squad/vehicle/flag/position reads, typed RCON economy/gift commands, and guarded XML mutations. The negotiation route never dispatches a Run job and never treats another Run binding, plugin version, adapter version, schema fingerprint, database identity, or asset digest as compatible evidence.
|
||||
|
||||
## Probe request
|
||||
|
||||
`SCUMSchemaProbeRequest` is a Platform durable-job payload addressed through the active authenticated Run binding.
|
||||
|
||||
Required fields:
|
||||
|
||||
- `requestId`, `jobId`;
|
||||
- `binding`: `serverInstanceId`, `runBindingId`, `runEndpointId`, `pluginId`, `pluginVersion`, `adapterVersion`, `gameVersion`, `databaseIdentity`;
|
||||
- `bounds`: `maxObjects`, `maxColumnsPerObject`, `maxIndexesPerObject`, `maxForeignKeys`, `maxCardinalityReads`, `maxSampleRows`, `timeoutMs`, `maxResultBytes`;
|
||||
- `requestedAt`.
|
||||
|
||||
The payload must not include a host database path, DSN, socket, credential, raw SQL text, raw rows, or SCUM-specific table names. Target resolution happens inside the active Run package from logical bindings only.
|
||||
|
||||
## Probe result
|
||||
|
||||
`SCUMSchemaProbeResult` returns only redacted schema evidence:
|
||||
|
||||
- request/job/binding identity;
|
||||
- status: `missing`, `compatible`, `incompatible`, or `failed`;
|
||||
- schema fingerprint and result digest;
|
||||
- bounded object metadata with object/name/column/index/fk fingerprints, declared types, nullable/primary-key flags, approximate row counts, and sample fingerprints;
|
||||
- safe error code/message when failed;
|
||||
- limits actually applied.
|
||||
|
||||
Samples are hashes/fingerprints only. Raw row content, XML payloads, SQL, paths, DSNs, sockets, credentials, host names, IPs, and RCON text are never returned to Platform Web, plugin pages, AI prompts, or safe diagnostic fields.
|
||||
|
||||
## SQLite template request
|
||||
|
||||
`SCUMSQLiteTemplateRequest` is the Platform durable-job payload for read-only plugin-owned query assets after a capability-specific gate is compatible. Required fields are:
|
||||
|
||||
- `requestId`, `jobId`;
|
||||
- `binding`: `serverInstanceId`, `runBindingId`, `runEndpointId`, `pluginId`, `pluginVersion`, `adapterVersion`, `gameVersion`, `databaseIdentity`;
|
||||
- `capability`, limited to database-backed read capabilities such as player, squad, vehicle, flag, and position reads;
|
||||
- logical `targetKey`, `templateKey`, `adapterVersion`, `requiredSchemaFingerprint`, immutable `assetDigest`, and canonical `parameterDigest`;
|
||||
- scalar `parameters` bounded by `maxParameters` and validated against the plugin-declared parameter schema;
|
||||
- `bounds`: `maxParameters`, `maxRows`, `timeoutMs`, `busyTimeoutMs`, and `maxResultBytes`;
|
||||
- `requestedAt`.
|
||||
|
||||
The request never contains raw SQL, host/database paths, DSNs, sockets, credentials, raw XML, RCON text, browser-supplied table names, or undeclared parameters. Run resolves the logical target and packaged template inside the generated Run package.
|
||||
|
||||
## SQLite template result
|
||||
|
||||
`SCUMSQLiteTemplateResult` is the terminal envelope for `sqlite.template-query` results. Required fields are request/job/binding identity, status (`succeeded`, `failed`, or `cancelled`), read capability, target/template key, adapter version, schema fingerprint, asset digest, parameter digest, source fingerprint, observed time, result digest, row count, bounded rows, truncation flag, safe error, and limits actually applied.
|
||||
|
||||
Platform accepts rows only when the terminal envelope matches the leased durable job's binding, template key, adapter/schema fingerprint, asset digest, and parameter digest. Late, duplicate, mismatched, stale, unsafe, over-limit, or schema-invalid results remain safe terminal failures and must not be converted into empty successful generations.
|
||||
|
||||
## Typed RCON template request
|
||||
|
||||
`SCUMTypedRCONTemplateRequest` is the Platform durable-job payload for plugin-owned command templates after a write capability is proven and reviewed. Required fields are:
|
||||
|
||||
- `requestId`, `jobId`;
|
||||
- `binding`: `serverInstanceId`, `runBindingId`, `runEndpointId`, `pluginId`, `pluginVersion`, `adapterVersion`, `gameVersion`, `databaseIdentity`;
|
||||
- `capability`, limited to verified typed RCON write capabilities such as economy-command or gift-command writes;
|
||||
- logical `transportKey`, `targetKey`, `templateKey`, `adapterVersion`, optional `requiredSchemaFingerprint`, immutable `assetDigest`, canonical `payloadDigest`, `confirmationDigest`, and `targetIdentityDigest`;
|
||||
- `idempotencyKey`, scalar `payload` validated against the plugin-declared payload schema, and safe `reviewReason`;
|
||||
- `bounds`: `maxPayloadBytes`, `timeoutMs`, `maxResponseBytes`, and `maxConfirmRecords`;
|
||||
- `requestedAt`.
|
||||
|
||||
The request never contains browser command text, raw RCON, SQL, XML, host/database paths, DSNs, sockets, credentials, or undeclared command keys. Run resolves the template and protected RCON transport inside the generated Run package.
|
||||
|
||||
## Typed RCON template result
|
||||
|
||||
`SCUMTypedRCONTemplateResult` is the terminal envelope for `rcon.template-command` results. Required fields are request/job/binding identity, status (`succeeded`, `failed`, or `cancelled`), write capability, transport/target/template key, adapter version, schema fingerprint when required, asset digest, payload digest, confirmation digest, target identity digest, observed time, result digest, response digest, confirmation status, confirmation digest id, safe summary, safe error, and limits actually applied.
|
||||
|
||||
Platform accepts write success only when the envelope matches the leased durable job and the declared confirmation status is conclusive. Missing, mismatched, stale, unsafe, partial, timed-out, cancelled, or schema-invalid confirmations remain failed or unknown outcomes; they must not update local verified facts or trigger automatic redelivery.
|
||||
|
||||
## Parsed log batch result
|
||||
|
||||
`SCUMParsedLogBatchResult` is the terminal envelope for `log.parsed-events` batches produced from plugin-declared log-source tailing or backfill. Required fields are:
|
||||
|
||||
- `requestId`, `jobId`;
|
||||
- `binding`: `serverInstanceId`, `runBindingId`, `runEndpointId`, `pluginId`, `pluginVersion`, `adapterVersion`, `gameVersion`, `databaseIdentity` for active-service fencing;
|
||||
- status (`succeeded`, `failed`, or `cancelled`), logical `sourceKey`, `streamKey`, `parserKey`, `parserVersion`, `adapterVersion`, immutable parser `assetDigest`, `parserDigest`, observed time, and `resultDigest`;
|
||||
- `firstCursor` and `lastCursor` containing `sourceIdentityDigest`, `streamGeneration`, and `sequence`;
|
||||
- `tailState`, limited to `advanced`, `rotated`, `truncated`, `restarted`, `partial-buffered`, or `replayed`, plus `partialLineBuffered` and `replay` flags;
|
||||
- `eventCount`, bounded sanitized events, safe summary, safe error, and limits: `maxEvents`, `maxPayloadBytes`, `maxLineBytes`, and `maxResultBytes`.
|
||||
|
||||
Each parsed event contains only event type, occurrence time, the transport cursor, privacy-safe `logicalEventDigest`, `eventDigest`, `payloadDigest`, and schema-safe scalar payload values. The envelope never contains raw log lines, raw IP/network identifiers, host paths, resolved file names, glob patterns, sockets, credentials, SQL, XML, or unredacted player/network identities.
|
||||
|
||||
Platform accepts a parsed-log success only when the envelope matches the leased job, server/Run binding, declared source/stream key, frozen parser key/version/digest when present, and a single redacted source identity/generation boundary. Replayed logical events are handled by the later ingestion layer through `logicalEventDigest`; transport cursor replay or rotation overlap must not by itself create duplicate players or sessions.
|
||||
|
||||
## Guarded mutation request
|
||||
|
||||
`SCUMGuardedMutationRequest` is the Platform durable-job payload for plugin-owned single-row SQLite/XML mutation templates after the mutation capability is proven, reviewed, and explicitly confirmed. Required fields are:
|
||||
|
||||
- `requestId`, `jobId`;
|
||||
- `binding`: `serverInstanceId`, `runBindingId`, `runEndpointId`, `pluginId`, `pluginVersion`, `adapterVersion`, `gameVersion`, `databaseIdentity`;
|
||||
- `capability`, limited to guarded database/XML write capabilities such as `profile-xml.write`;
|
||||
- logical `targetKey`, `templateKey`, `adapterVersion`, `requiredSchemaFingerprint`, immutable `assetDigest`, `targetIdentityDigest`, `expectedRowDigest`, `expectedValueDigest`, `expectedXmlDigest`, `patchDigest`, `backupEvidenceDigest`, `offlineEvidenceDigest`, `dangerConfirmationDigest`, and `readbackExpectationDigest`;
|
||||
- `idempotencyKey`, scalar `payload` validated against the plugin-declared payload schema, and safe `reviewReason`;
|
||||
- `bounds`: `maxPayloadBytes`, `timeoutMs`, `busyTimeoutMs`, `maxReadbackBytes`, and `maxAffectedRows`, which must equal `1`;
|
||||
- `requestedAt`.
|
||||
|
||||
The request never contains raw SQL, raw XML, browser mutation text, host/database paths, DSNs, sockets, credentials, table/column overrides, raw row payloads, `fieldKey=855`, `prisoner.value`, or undeclared patch fields. Run resolves the logical target and packaged preserving patch template inside the generated Run package.
|
||||
|
||||
## Guarded mutation result
|
||||
|
||||
`SCUMGuardedMutationResult` is the terminal envelope for `sqlite.guarded-mutation` results. Required fields are request/job/binding identity, status (`succeeded`, `failed`, or `cancelled`), write capability, target/template key, adapter version, schema fingerprint, asset digest, source fingerprint when succeeded, target identity digest, expected row/value/XML digests, patch digest, backup/offline/danger-confirmation digests, readback expectation digest, observed time, result digest, before/after/readback digests when succeeded, affected-row count, readback status, safe summary, safe error, and limits actually applied.
|
||||
|
||||
Platform accepts mutation success only when the terminal envelope matches the leased durable job and the declared binding/template/schema/asset/target/guard/patch/backup/offline/confirmation/readback digests, `affectedRows` is exactly `1`, and readback is `confirmed`. Zero rows, multiple rows, stale expected values, schema or source changes, malformed XML, absent named nodes, rollback, missing backup/offline/danger confirmation, missing readback, or unsafe summaries remain failed/conflict/unknown outcomes and must not update local verified facts.
|
||||
|
||||
## Release behavior
|
||||
|
||||
The first-party SCUM plugin declares `scumLiveData` with `remote.run.db.sqlite.probe` and per-capability gates. Until current-service evidence exists, all gates remain `disabled` with `evidenceStatus: missing`. Query assets, RCON templates, XML mutations, map transforms, and gift transports may be added only after current-service probe evidence proves their adapter requirements; unsupported or ambiguous capabilities stay disabled independently.
|
||||
@@ -1,77 +1,67 @@
|
||||
# SCUM Run Integration Contract
|
||||
|
||||
This repository owns the Platform/plugin side of SCUM real-data operations. The machine-side executor remains the independent `git@git.npc0.com:admin343/run.git` repository, and no `run/` source tree or SCUM-specific executor branch belongs in this repository.
|
||||
This repository defines the platform/plugin side of SCUM real-data operations. The executable machine-side implementation belongs in the independent `git@git.npc0.com:admin343/run.git` repository and must not be added here.
|
||||
|
||||
## Ownership Boundary
|
||||
|
||||
- Platform owns server instances, target-server authorization, durable jobs, local SCUM records, capability evidence, generated Run package inputs, safe browser APIs, idempotency, and internal write confirmation evidence.
|
||||
- The SCUM plugin owns versioned parser declarations, SQLite template assets, result schemas, schema-adapter compatibility, map metadata, typed command templates, gift catalogs, and guarded mutation declarations.
|
||||
- Run owns generic machine-side execution beside the current bound service: resolving package-scoped logical targets, enforcing declared capabilities, executing bounded jobs, supervising declared log sources, and returning terminal envelopes through the existing signed channels.
|
||||
- Platform owns server instances, authorization, audit, local projections, typed operation/workflow records, idempotency, approval state, and safe browser APIs.
|
||||
- The SCUM plugin owns query template keys, operation template keys, result schemas, safety rules, confirmation schemas, and lifecycle action assets.
|
||||
- Run owns local machine execution beside the current SCUM service: locating the declared logical SCUM.db/log/RCON targets from its scoped package, executing bounded jobs, and returning typed results through existing signed job channels.
|
||||
|
||||
Run and Platform Web must never receive or expose raw SQL, raw RCON text, raw XML, host/database paths, DSNs, sockets, credentials, raw row content, IP/network material, or arbitrary browser-supplied execution payloads.
|
||||
Run must never send host paths, DSNs, sockets, credentials, raw SQL, raw RCON text, or protected request bodies to browser/product APIs. Platform persists only safe job metadata, projection rows, checksums, confirmation summaries, and audit references.
|
||||
|
||||
## Capability Gate
|
||||
## Read Observation Jobs
|
||||
|
||||
Every database-backed SCUM read or write capability stays disabled until the active Run binding reports compatible current-service evidence for that exact server, endpoint, binding, plugin version, adapter version, game version, database identity, schema fingerprint, and asset digest set.
|
||||
Run must implement plugin-declared SQLite read templates for the current server binding and return rows matching the referenced schema files under `plugins/examples/scum-server-plugin/schemas/bridge/queries/`.
|
||||
|
||||
Disabled capability results are ordinary safe availability states such as `probe_executor_absent`, `probe_missing`, `schema_incompatible`, `binding_mismatch`, `fingerprint_mismatch`, `digest_mismatch`, or `evidence_expired`. They are not Workflow, observation, projection, audit-initiation, or manual-refresh states.
|
||||
Required template keys:
|
||||
|
||||
## Schema Probe Jobs
|
||||
| Key | Required behavior |
|
||||
| --- | --- |
|
||||
| `scum.player.profile` | Read player identity, profile ID, optional Steam/user ID, character/prisoner fields, economy balances, squad summary, and current coordinates where available. |
|
||||
| `scum.squads` | Read squad IDs, names, leader/profile references, and bounded member counts. |
|
||||
| `scum.squad-members` | Read roster membership, ranks, player/profile references, and unknown fields without fabricating missing identities. |
|
||||
| `scum.vehicles` | Read vehicle/entity rows and coordinates; unknown class/name mappings remain unknown. |
|
||||
| `scum.flags` | Read base flag/entity ownership, squad/player confidence, and coordinates where available. |
|
||||
| `scum.positions` | Read current player, vehicle, and flag coordinate projections. |
|
||||
|
||||
Platform may dispatch a schema probe only as a durable job through the active authenticated Run binding. The probe payload contains a logical target key, binding identity, timeout/row/result bounds, and no SCUM table names, database path, SQL text, row values, XML, credentials, or host identifiers.
|
||||
Each successful result must include the server binding, template key, observed time, monotonically comparable sequence, row count within manifest bounds, and `sha256:<hex>` checksum. Failures must return safe error codes such as missing database, locked database, schema mismatch, timeout, or row-bound exceeded; platform will mark affected projections stale while keeping last-known-good records.
|
||||
|
||||
Run executes the generic `remote.run.db.sqlite.probe` capability against the package-resolved current database or a short-lived read-only snapshot fenced to the same binding/database identity. The terminal result returns only redacted object, column, index, foreign-key, approximate cardinality, and sample fingerprints with the applied limits and a safe status.
|
||||
Login/logout evidence comes from plugin-declared log sources. A login line can create/update a local player/session projection; `last_save_time` is only freshness evidence and must not be treated as online-state proof by itself.
|
||||
|
||||
## Read-Only SQLite Template Jobs
|
||||
## Controlled Write Jobs
|
||||
|
||||
After probe evidence matches a plugin adapter, Platform can schedule plugin-owned read-only SQLite template jobs by template key, adapter/schema version, immutable asset digest, and bounded parameters. Platform does not build SQL strings, and the browser never submits query text or undeclared parameters.
|
||||
Run must execute only typed operations declared by the SCUM plugin manifest.
|
||||
|
||||
The leased Run assignment carries a typed `sqliteTemplate` request only. Required fields are `requestId`, server/plugin binding, read capability, logical `targetKey`, `templateKey`, `adapterVersion`, `requiredSchemaFingerprint`, immutable `assetDigest`, canonical `parameterDigest`, bounded scalar `parameters`, and `limits` containing `maxParameters`, `maxRows`, `timeoutMs`, `busyTimeoutMs`, and `maxResultBytes`. The payload carries no SQL text, table names from the browser, database path, DSN, socket, credential, raw XML, RCON text, or host identifier.
|
||||
| Operation key | Transport | Required behavior |
|
||||
| --- | --- | --- |
|
||||
| `player.fame.set` | RCON | Use the declared command template for fame and confirm through follow-up readback. |
|
||||
| `player.currency.normal.set` | RCON | Use the declared command template for normal currency and confirm through follow-up readback. |
|
||||
| `player.currency.gold.set` | RCON | Use the declared command template for gold and confirm through follow-up readback. |
|
||||
| `player.notify` | RCON/declared notification command | Deliver bounded player notification text and report unknown if delivery cannot be proven. |
|
||||
| `reward.deliver` | Declared reward transport | Deliver catalogued reward/notification only once per idempotency key and confirmation state. |
|
||||
| `player.attribute.855.set` | SQLite mutation | Execute the declared DB-only mutation with before-value guard, max affected rows = 1, maintenance/offline evidence, backup/snapshot reference, and confirmation query. |
|
||||
|
||||
Run verifies the packaged asset digest, adapter/schema fingerprint, canonical parameter digest, and active binding before opening a query-only SQLite connection or fenced short-lived read-only snapshot. It enforces one approved read-only statement or introspection boundary, bound parameters, short busy/operation timeouts, cancellation, row/result-byte limits, and rejects DDL, mutation, `ATTACH`, extension loading, write PRAGMAs, multi-statement input, and string-concatenated parameters.
|
||||
RCON-supported fame/currency writes must not be converted to DB mutations. DB-only mutations must fail safely when the current value differs from the approved `before` value, the affected row bound is exceeded, backup evidence is missing, or the player safety state is online/unknown.
|
||||
|
||||
The terminal `sqlite.template-query` envelope contains `requestId`, `jobId`, binding, status (`succeeded`, `failed`, or `cancelled`), capability, target/template key, adapter version, schema fingerprint, asset digest, parameter digest, source fingerprint, observed time, result digest, row count, bounded rows, truncation flag, safe error, and applied limits. Platform validates the envelope against the original durable job, lease attempt, binding, template key, schema fingerprint, asset digest, and parameter digest before any local SCUM generation can consume the rows.
|
||||
## Result And Confirmation Contract
|
||||
|
||||
## Parsed Log Event Jobs
|
||||
Run job results for SCUM reads, RCON writes, and SQLite mutations must return:
|
||||
|
||||
Login/logout ingestion starts from plugin-declared log sources and versioned parser assets. Each parsed event carries server/plugin/parser identity, transport cursor `(source identity, stream generation, sequence)`, a separate privacy-safe logical event identity, and occurrence time.
|
||||
- `kind` identifying the declared result type.
|
||||
- `checksum` as `sha256:<64 hex chars>`.
|
||||
- Bounded JSON content matching the plugin result/confirmation schema.
|
||||
- `affectedRows` for mutations and zero/one row confirmation details where applicable.
|
||||
- A safe audit summary that excludes raw SQL, raw RCON text, SCUM.db paths, host paths, tokens, sockets, and credentials.
|
||||
|
||||
Run and Platform discard raw IP addresses and other network identifiers before durable storage or logical fingerprinting. Malformed, failed-login, obsolete-binding, duplicate, or out-of-order events must not fabricate players or sessions.
|
||||
If execution may have happened but confirmation is missing, run should report an unknown/pending-confirmation state rather than success. Platform will read back before retrying so gifts, currency, fame, and DB fields are not duplicated or overwritten.
|
||||
|
||||
For plugin-declared file-tail backfill or replay, the leased Run assignment carries the frozen declared `logSource` and may additionally freeze `parserKey`, `parserVersion`, `parserDigest`, and `adapterVersion` as safe scalar execution inputs. The log source contains only package logical `sourceKey`/`targetKey`/`streamKey` metadata, cursor kind, and retention policy. It never contains the resolved host log path, glob, socket, credential, network endpoint, or raw line material.
|
||||
## External Run Tasks
|
||||
|
||||
The terminal `log.parsed-events` envelope contains request/job identity, server/plugin binding, status, source/stream key, parser key/version, adapter version, immutable parser asset digest, parser digest, observed time, result digest, first/last transport cursors with redacted `sourceIdentityDigest`, stream generation, sequence, tail state (`advanced`, `rotated`, `truncated`, `restarted`, `partial-buffered`, or `replayed`), partial-line and replay flags, event count, bounded sanitized events, safe summary, safe error, and applied limits. Each event carries event type, occurrence time, transport cursor, logical event digest, event digest, payload digest, and schema-safe scalar payload values only.
|
||||
The independent run repository needs implementation work for:
|
||||
|
||||
Platform accepts the parsed-log envelope only when it matches the leased `logs.backfill` job, active server/Run endpoint, plugin id/version when frozen, declared source/stream key, parser key/version/digest when frozen, and a single source identity/generation boundary. Parser digests, source identity, stream generation, logical event digest, and payload digest are fingerprints; raw log lines, IP/network values, paths, SQL, XML, sockets, credentials, and player identities not already redacted are rejected before local ingestion can use the batch.
|
||||
|
||||
## Typed RCON Template Jobs
|
||||
|
||||
SCUM command writes use only plugin-owned typed command templates. Platform dispatches a template key, adapter version, digest, target identity, idempotency key, validated parameters, and review reason through the durable job channel.
|
||||
|
||||
The leased Run assignment carries a typed `rconTemplate` request only. Required fields are `requestId`, server/plugin binding, write capability, logical `transportKey`, logical `targetKey`, `templateKey`, `adapterVersion`, optional `requiredSchemaFingerprint`, immutable `assetDigest`, canonical `payloadDigest`, `confirmationDigest`, `targetIdentityDigest`, idempotency key, bounded scalar payload, review reason, and limits containing `maxPayloadBytes`, `timeoutMs`, `maxResponseBytes`, and `maxConfirmRecords`. The payload carries no browser command text, raw RCON, SQL, XML, host path, socket, credential, or undeclared command key.
|
||||
|
||||
Run resolves the packaged command template and protected RCON transport from the generated Run package, verifies the asset/payload/confirmation digests and active binding, renders only the packaged template with bound scalar payload values, executes through generic protected RCON, and performs only the declared confirmation path. Run never accepts browser command text, exposes the rendered command in result envelopes, or branches on SCUM command names, SCUM keys, SCUM commands, SCUM tables, or gift/economy semantics.
|
||||
|
||||
The terminal `rcon.template-command` envelope contains `requestId`, `jobId`, binding, status (`succeeded`, `failed`, or `cancelled`), capability, transport/target/template key, adapter version, schema fingerprint when required, asset digest, payload digest, confirmation digest, target identity digest, observed time, result digest, response digest, confirmation status, confirmation digest id, safe summary, safe error, and applied limits. A write is successful only after the declared confirmation path returns schema-valid conclusive evidence; missing, partial, mismatched, cancelled, or timed-out confirmation is reported as failed, partial, or unknown rather than success.
|
||||
|
||||
## Guarded SQLite/XML Mutation Jobs
|
||||
|
||||
Database/XML writes are disabled until current-service evidence proves the source row, XML field, named attribute mapping, backup/offline safety requirements, and preserving patch contract. `855` is never an executable field key; it may only be a reviewed preset label that expands to explicit named attributes after the mapping is confirmed.
|
||||
|
||||
Platform dispatches guarded mutations only with effective `server.game-client.maintenance`, explicit danger confirmation, target identity, expected before values/checksum, same-instance backup evidence, idempotency key, reason, adapter/digest, and declared safety requirements. Run performs one bounded transaction, updates exactly one guarded row, preserves untargeted XML content, rolls back on zero/multiple affected rows or conflicts, and performs read-after-write confirmation before any success result.
|
||||
|
||||
The leased Run assignment carries a typed `guardedMutation` request only. Required fields are `requestId`, server/plugin binding, `profile-xml.write` capability, logical `targetKey`, `templateKey`, `adapterVersion`, `requiredSchemaFingerprint`, immutable `assetDigest`, `targetIdentityDigest`, `expectedRowDigest`, `expectedValueDigest`, `expectedXmlDigest`, `patchDigest`, `backupEvidenceDigest`, `offlineEvidenceDigest`, `dangerConfirmationDigest`, `readbackExpectationDigest`, idempotency key, bounded scalar payload, review reason, and limits containing `maxPayloadBytes`, `timeoutMs`, `busyTimeoutMs`, `maxReadbackBytes`, and `maxAffectedRows=1`. The payload carries no raw SQL, raw XML, database path, table/column override, `855` field key, browser mutation text, host path, socket, credential, or undeclared patch field.
|
||||
|
||||
The terminal `sqlite.guarded-mutation` envelope contains `requestId`, `jobId`, binding, status (`succeeded`, `failed`, or `cancelled`), capability, target/template key, adapter/schema fingerprint, asset digest, source fingerprint, target identity digest, expected row/value/XML digests, patch digest, backup/offline/danger-confirmation digests, readback expectation digest, observed time, result digest, before/after/readback digests, affected-row count, readback status, safe summary, safe error, and applied limits. Platform accepts success only when the envelope matches the leased job and binding, `affectedRows` is exactly `1`, and `readbackStatus` is `confirmed`; zero/multiple rows, guard mismatches, malformed XML, missing backup/offline/danger confirmation, missing readback, or stale schema remain safe failed/conflict/unknown results.
|
||||
|
||||
Saving attributes must never implicitly kill, respawn, kick, or otherwise activate destructive game behavior. Any verified required activation is a separate permission-checked and explicitly confirmed typed command.
|
||||
|
||||
## Terminal Result Envelope
|
||||
|
||||
Every probe, read template, typed command, parsed-log batch, or guarded mutation result returns a typed terminal envelope containing server/plugin binding, adapter/schema version, template/action/parser key, asset digest, job identity, observed time, checksum/result digest, row or affected-row count where applicable, and a stable safe result/error code.
|
||||
|
||||
Platform validates the envelope against the original durable job before updating local SCUM records or write-confirmation state. Late, duplicate, foreign, stale, incompatible, or unsafe results are rejected idempotently while preserving the last completed local generation.
|
||||
|
||||
## External Run Evidence Required
|
||||
|
||||
The independent Run repository still needs separately authorized implementation and verification evidence for generic schema probing, packaged SQLite-template execution, typed RCON execution, guarded SQLite/XML mutation execution, plugin-declared log-source tailing, and terminal-envelope fencing. This browser repository must record that tested Run commit/deployment evidence before enabling database-backed adapters, adding production query/mutation assets, or marking the real-service verification tasks complete.
|
||||
1. Resolve package-scoped logical SCUM.db and log targets from the generated run plan without exposing resolved host paths to Platform Web.
|
||||
2. Execute the six declared SQLite read templates with row/time bounds and schema-compatible JSON rows.
|
||||
3. Execute typed RCON operation templates for fame, currency, notification, and reward delivery without accepting arbitrary browser command text.
|
||||
4. Execute `player.attribute.855.set` through a guarded SQLite mutation with backup, maintenance/offline checks, before-value match, affected-row bound, and confirmation read.
|
||||
5. Report observation failures and write unknown states with safe codes and checksums so platform projections and workflows can reconcile deterministically.
|
||||
|
||||
@@ -53,6 +53,16 @@ type StoreSnapshot struct {
|
||||
GameGiftCatalogs []domain.GameGiftCatalog `json:"gameGiftCatalogs"`
|
||||
GameGiftRevisions []domain.GameGiftRevision `json:"gameGiftRevisions"`
|
||||
GameGiftGrants []domain.GameGiftGrant `json:"gameGiftGrants"`
|
||||
SCUMDataObservations []domain.SCUMDataObservation `json:"scumDataObservations"`
|
||||
SCUMPlayerLiveStates []domain.SCUMPlayerLiveState `json:"scumPlayerLiveStates"`
|
||||
SCUMSquads []domain.SCUMSquad `json:"scumSquads"`
|
||||
SCUMSquadMembers []domain.SCUMSquadMember `json:"scumSquadMembers"`
|
||||
SCUMVehicles []domain.SCUMVehicle `json:"scumVehicles"`
|
||||
SCUMFlags []domain.SCUMFlag `json:"scumFlags"`
|
||||
SCUMCurrentPositions []domain.SCUMCurrentPosition `json:"scumCurrentPositions"`
|
||||
SCUMOperationRequests []domain.SCUMOperationRequest `json:"scumOperationRequests"`
|
||||
SCUMWorkflowInstances []domain.SCUMWorkflowInstance `json:"scumWorkflowInstances"`
|
||||
SCUMWorkflowSteps []domain.SCUMWorkflowStep `json:"scumWorkflowSteps"`
|
||||
}
|
||||
|
||||
type FileStore struct {
|
||||
@@ -234,6 +244,36 @@ func (store *FileStore) GameGiftRevisions() GameGiftRevisionRepository {
|
||||
func (store *FileStore) GameGiftGrants() GameGiftGrantRepository {
|
||||
return &persistentRepository[domain.GameGiftGrant, domain.GameGiftGrantFilter]{repository: store.MemoryStore.gameGiftGrants, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMDataObservations() SCUMDataObservationRepository {
|
||||
return &persistentRepository[domain.SCUMDataObservation, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumDataObservations, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMPlayerLiveStates() SCUMPlayerLiveStateRepository {
|
||||
return &persistentRepository[domain.SCUMPlayerLiveState, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumPlayerLiveStates, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMSquads() SCUMSquadRepository {
|
||||
return &persistentRepository[domain.SCUMSquad, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumSquads, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMSquadMembers() SCUMSquadMemberRepository {
|
||||
return &persistentRepository[domain.SCUMSquadMember, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumSquadMembers, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMVehicles() SCUMVehicleRepository {
|
||||
return &persistentRepository[domain.SCUMVehicle, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumVehicles, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMFlags() SCUMFlagRepository {
|
||||
return &persistentRepository[domain.SCUMFlag, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumFlags, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMCurrentPositions() SCUMCurrentPositionRepository {
|
||||
return &persistentRepository[domain.SCUMCurrentPosition, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumCurrentPositions, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMOperationRequests() SCUMOperationRequestRepository {
|
||||
return &persistentRepository[domain.SCUMOperationRequest, domain.SCUMOperationRequestFilter]{repository: store.MemoryStore.scumOperationRequests, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMWorkflowInstances() SCUMWorkflowInstanceRepository {
|
||||
return &persistentRepository[domain.SCUMWorkflowInstance, domain.SCUMWorkflowInstanceFilter]{repository: store.MemoryStore.scumWorkflowInstances, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMWorkflowSteps() SCUMWorkflowStepRepository {
|
||||
return &persistentRepository[domain.SCUMWorkflowStep, domain.SCUMWorkflowStepFilter]{repository: store.MemoryStore.scumWorkflowSteps, persist: store.persist}
|
||||
}
|
||||
|
||||
func (store *FileStore) load() error {
|
||||
data, err := os.ReadFile(store.path)
|
||||
@@ -307,7 +347,7 @@ func (store *FileStore) snapshot() StoreSnapshot {
|
||||
GameClientBridgeCommands: snapshotRepository(store.MemoryStore.bridgeCommands.memoryRepository),
|
||||
GameClientBridgeSnapshots: snapshotRepository(store.MemoryStore.bridgeSnapshots.memoryRepository),
|
||||
GameClientBridgeStreams: snapshotRepository(store.MemoryStore.bridgeStreams),
|
||||
GamePlayers: snapshotRepository(store.MemoryStore.gamePlayers), GamePlayerAliases: snapshotRepository(store.MemoryStore.gamePlayerAliases), GamePlayerSessions: snapshotRepository(store.MemoryStore.gamePlayerSessions), GameAccessAttempts: snapshotRepository(store.MemoryStore.gameAccessAttempts), GameSecuritySignals: snapshotRepository(store.MemoryStore.gameSecuritySignals), GamePlayerStatePatches: snapshotRepository(store.MemoryStore.gamePlayerStatePatches), GameMapTrackPoints: snapshotRepository(store.MemoryStore.gameMapTrackPoints), GamePlayerVehicleSegments: snapshotRepository(store.MemoryStore.gamePlayerVehicleSegments), GameGiftCatalogs: snapshotRepository(store.MemoryStore.gameGiftCatalogs), GameGiftRevisions: snapshotRepository(store.MemoryStore.gameGiftRevisions), GameGiftGrants: snapshotRepository(store.MemoryStore.gameGiftGrants),
|
||||
GamePlayers: snapshotRepository(store.MemoryStore.gamePlayers), GamePlayerAliases: snapshotRepository(store.MemoryStore.gamePlayerAliases), GamePlayerSessions: snapshotRepository(store.MemoryStore.gamePlayerSessions), GameAccessAttempts: snapshotRepository(store.MemoryStore.gameAccessAttempts), GameSecuritySignals: snapshotRepository(store.MemoryStore.gameSecuritySignals), GamePlayerStatePatches: snapshotRepository(store.MemoryStore.gamePlayerStatePatches), GameMapTrackPoints: snapshotRepository(store.MemoryStore.gameMapTrackPoints), GamePlayerVehicleSegments: snapshotRepository(store.MemoryStore.gamePlayerVehicleSegments), GameGiftCatalogs: snapshotRepository(store.MemoryStore.gameGiftCatalogs), GameGiftRevisions: snapshotRepository(store.MemoryStore.gameGiftRevisions), GameGiftGrants: snapshotRepository(store.MemoryStore.gameGiftGrants), SCUMDataObservations: snapshotRepository(store.MemoryStore.scumDataObservations), SCUMPlayerLiveStates: snapshotRepository(store.MemoryStore.scumPlayerLiveStates), SCUMSquads: snapshotRepository(store.MemoryStore.scumSquads), SCUMSquadMembers: snapshotRepository(store.MemoryStore.scumSquadMembers), SCUMVehicles: snapshotRepository(store.MemoryStore.scumVehicles), SCUMFlags: snapshotRepository(store.MemoryStore.scumFlags), SCUMCurrentPositions: snapshotRepository(store.MemoryStore.scumCurrentPositions), SCUMOperationRequests: snapshotRepository(store.MemoryStore.scumOperationRequests), SCUMWorkflowInstances: snapshotRepository(store.MemoryStore.scumWorkflowInstances), SCUMWorkflowSteps: snapshotRepository(store.MemoryStore.scumWorkflowSteps),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,6 +392,16 @@ func (store *FileStore) loadSnapshot(snapshot StoreSnapshot) {
|
||||
loadRepository(store.MemoryStore.gameGiftCatalogs, snapshot.GameGiftCatalogs)
|
||||
loadRepository(store.MemoryStore.gameGiftRevisions, snapshot.GameGiftRevisions)
|
||||
loadRepository(store.MemoryStore.gameGiftGrants, snapshot.GameGiftGrants)
|
||||
loadRepository(store.MemoryStore.scumDataObservations, snapshot.SCUMDataObservations)
|
||||
loadRepository(store.MemoryStore.scumPlayerLiveStates, snapshot.SCUMPlayerLiveStates)
|
||||
loadRepository(store.MemoryStore.scumSquads, snapshot.SCUMSquads)
|
||||
loadRepository(store.MemoryStore.scumSquadMembers, snapshot.SCUMSquadMembers)
|
||||
loadRepository(store.MemoryStore.scumVehicles, snapshot.SCUMVehicles)
|
||||
loadRepository(store.MemoryStore.scumFlags, snapshot.SCUMFlags)
|
||||
loadRepository(store.MemoryStore.scumCurrentPositions, snapshot.SCUMCurrentPositions)
|
||||
loadRepository(store.MemoryStore.scumOperationRequests, snapshot.SCUMOperationRequests)
|
||||
loadRepository(store.MemoryStore.scumWorkflowInstances, snapshot.SCUMWorkflowInstances)
|
||||
loadRepository(store.MemoryStore.scumWorkflowSteps, snapshot.SCUMWorkflowSteps)
|
||||
}
|
||||
|
||||
type mutableRepository[T any, F any] interface {
|
||||
|
||||
@@ -204,6 +204,36 @@ func (store *MySQLStore) GameGiftRevisions() GameGiftRevisionRepository {
|
||||
func (store *MySQLStore) GameGiftGrants() GameGiftGrantRepository {
|
||||
return &persistentRepository[domain.GameGiftGrant, domain.GameGiftGrantFilter]{repository: store.MemoryStore.gameGiftGrants, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMDataObservations() SCUMDataObservationRepository {
|
||||
return &persistentRepository[domain.SCUMDataObservation, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumDataObservations, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMPlayerLiveStates() SCUMPlayerLiveStateRepository {
|
||||
return &persistentRepository[domain.SCUMPlayerLiveState, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumPlayerLiveStates, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMSquads() SCUMSquadRepository {
|
||||
return &persistentRepository[domain.SCUMSquad, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumSquads, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMSquadMembers() SCUMSquadMemberRepository {
|
||||
return &persistentRepository[domain.SCUMSquadMember, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumSquadMembers, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMVehicles() SCUMVehicleRepository {
|
||||
return &persistentRepository[domain.SCUMVehicle, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumVehicles, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMFlags() SCUMFlagRepository {
|
||||
return &persistentRepository[domain.SCUMFlag, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumFlags, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMCurrentPositions() SCUMCurrentPositionRepository {
|
||||
return &persistentRepository[domain.SCUMCurrentPosition, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumCurrentPositions, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMOperationRequests() SCUMOperationRequestRepository {
|
||||
return &persistentRepository[domain.SCUMOperationRequest, domain.SCUMOperationRequestFilter]{repository: store.MemoryStore.scumOperationRequests, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMWorkflowInstances() SCUMWorkflowInstanceRepository {
|
||||
return &persistentRepository[domain.SCUMWorkflowInstance, domain.SCUMWorkflowInstanceFilter]{repository: store.MemoryStore.scumWorkflowInstances, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMWorkflowSteps() SCUMWorkflowStepRepository {
|
||||
return &persistentRepository[domain.SCUMWorkflowStep, domain.SCUMWorkflowStepFilter]{repository: store.MemoryStore.scumWorkflowSteps, persist: store.persist}
|
||||
}
|
||||
|
||||
func (store *MySQLStore) initialize() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
@@ -294,7 +324,7 @@ func (store *MySQLStore) snapshot() StoreSnapshot {
|
||||
GameClientBridgeCommands: snapshotRepository(store.MemoryStore.bridgeCommands.memoryRepository),
|
||||
GameClientBridgeSnapshots: snapshotRepository(store.MemoryStore.bridgeSnapshots.memoryRepository),
|
||||
GameClientBridgeStreams: snapshotRepository(store.MemoryStore.bridgeStreams),
|
||||
GamePlayers: snapshotRepository(store.MemoryStore.gamePlayers), GamePlayerAliases: snapshotRepository(store.MemoryStore.gamePlayerAliases), GamePlayerSessions: snapshotRepository(store.MemoryStore.gamePlayerSessions), GameAccessAttempts: snapshotRepository(store.MemoryStore.gameAccessAttempts), GameSecuritySignals: snapshotRepository(store.MemoryStore.gameSecuritySignals), GamePlayerStatePatches: snapshotRepository(store.MemoryStore.gamePlayerStatePatches), GameMapTrackPoints: snapshotRepository(store.MemoryStore.gameMapTrackPoints), GamePlayerVehicleSegments: snapshotRepository(store.MemoryStore.gamePlayerVehicleSegments), GameGiftCatalogs: snapshotRepository(store.MemoryStore.gameGiftCatalogs), GameGiftRevisions: snapshotRepository(store.MemoryStore.gameGiftRevisions), GameGiftGrants: snapshotRepository(store.MemoryStore.gameGiftGrants),
|
||||
GamePlayers: snapshotRepository(store.MemoryStore.gamePlayers), GamePlayerAliases: snapshotRepository(store.MemoryStore.gamePlayerAliases), GamePlayerSessions: snapshotRepository(store.MemoryStore.gamePlayerSessions), GameAccessAttempts: snapshotRepository(store.MemoryStore.gameAccessAttempts), GameSecuritySignals: snapshotRepository(store.MemoryStore.gameSecuritySignals), GamePlayerStatePatches: snapshotRepository(store.MemoryStore.gamePlayerStatePatches), GameMapTrackPoints: snapshotRepository(store.MemoryStore.gameMapTrackPoints), GamePlayerVehicleSegments: snapshotRepository(store.MemoryStore.gamePlayerVehicleSegments), GameGiftCatalogs: snapshotRepository(store.MemoryStore.gameGiftCatalogs), GameGiftRevisions: snapshotRepository(store.MemoryStore.gameGiftRevisions), GameGiftGrants: snapshotRepository(store.MemoryStore.gameGiftGrants), SCUMDataObservations: snapshotRepository(store.MemoryStore.scumDataObservations), SCUMPlayerLiveStates: snapshotRepository(store.MemoryStore.scumPlayerLiveStates), SCUMSquads: snapshotRepository(store.MemoryStore.scumSquads), SCUMSquadMembers: snapshotRepository(store.MemoryStore.scumSquadMembers), SCUMVehicles: snapshotRepository(store.MemoryStore.scumVehicles), SCUMFlags: snapshotRepository(store.MemoryStore.scumFlags), SCUMCurrentPositions: snapshotRepository(store.MemoryStore.scumCurrentPositions), SCUMOperationRequests: snapshotRepository(store.MemoryStore.scumOperationRequests), SCUMWorkflowInstances: snapshotRepository(store.MemoryStore.scumWorkflowInstances), SCUMWorkflowSteps: snapshotRepository(store.MemoryStore.scumWorkflowSteps),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,4 +369,14 @@ func (store *MySQLStore) loadSnapshot(snapshot StoreSnapshot) {
|
||||
loadRepository(store.MemoryStore.gameGiftCatalogs, snapshot.GameGiftCatalogs)
|
||||
loadRepository(store.MemoryStore.gameGiftRevisions, snapshot.GameGiftRevisions)
|
||||
loadRepository(store.MemoryStore.gameGiftGrants, snapshot.GameGiftGrants)
|
||||
loadRepository(store.MemoryStore.scumDataObservations, snapshot.SCUMDataObservations)
|
||||
loadRepository(store.MemoryStore.scumPlayerLiveStates, snapshot.SCUMPlayerLiveStates)
|
||||
loadRepository(store.MemoryStore.scumSquads, snapshot.SCUMSquads)
|
||||
loadRepository(store.MemoryStore.scumSquadMembers, snapshot.SCUMSquadMembers)
|
||||
loadRepository(store.MemoryStore.scumVehicles, snapshot.SCUMVehicles)
|
||||
loadRepository(store.MemoryStore.scumFlags, snapshot.SCUMFlags)
|
||||
loadRepository(store.MemoryStore.scumCurrentPositions, snapshot.SCUMCurrentPositions)
|
||||
loadRepository(store.MemoryStore.scumOperationRequests, snapshot.SCUMOperationRequests)
|
||||
loadRepository(store.MemoryStore.scumWorkflowInstances, snapshot.SCUMWorkflowInstances)
|
||||
loadRepository(store.MemoryStore.scumWorkflowSteps, snapshot.SCUMWorkflowSteps)
|
||||
}
|
||||
|
||||
@@ -295,6 +295,67 @@ type GameGiftGrantRepository interface {
|
||||
List(domain.GameGiftGrantFilter) ([]domain.GameGiftGrant, error)
|
||||
Update(domain.GameGiftGrant) error
|
||||
}
|
||||
type SCUMDataObservationRepository interface {
|
||||
Create(domain.SCUMDataObservation) error
|
||||
Get(string) (domain.SCUMDataObservation, error)
|
||||
List(domain.SCUMProjectionFilter) ([]domain.SCUMDataObservation, error)
|
||||
Update(domain.SCUMDataObservation) error
|
||||
}
|
||||
type SCUMPlayerLiveStateRepository interface {
|
||||
Create(domain.SCUMPlayerLiveState) error
|
||||
Get(string) (domain.SCUMPlayerLiveState, error)
|
||||
List(domain.SCUMProjectionFilter) ([]domain.SCUMPlayerLiveState, error)
|
||||
Update(domain.SCUMPlayerLiveState) error
|
||||
}
|
||||
type SCUMSquadRepository interface {
|
||||
Create(domain.SCUMSquad) error
|
||||
Get(string) (domain.SCUMSquad, error)
|
||||
List(domain.SCUMProjectionFilter) ([]domain.SCUMSquad, error)
|
||||
Update(domain.SCUMSquad) error
|
||||
}
|
||||
type SCUMSquadMemberRepository interface {
|
||||
Create(domain.SCUMSquadMember) error
|
||||
Get(string) (domain.SCUMSquadMember, error)
|
||||
List(domain.SCUMProjectionFilter) ([]domain.SCUMSquadMember, error)
|
||||
Update(domain.SCUMSquadMember) error
|
||||
}
|
||||
type SCUMVehicleRepository interface {
|
||||
Create(domain.SCUMVehicle) error
|
||||
Get(string) (domain.SCUMVehicle, error)
|
||||
List(domain.SCUMProjectionFilter) ([]domain.SCUMVehicle, error)
|
||||
Update(domain.SCUMVehicle) error
|
||||
}
|
||||
type SCUMFlagRepository interface {
|
||||
Create(domain.SCUMFlag) error
|
||||
Get(string) (domain.SCUMFlag, error)
|
||||
List(domain.SCUMProjectionFilter) ([]domain.SCUMFlag, error)
|
||||
Update(domain.SCUMFlag) error
|
||||
}
|
||||
type SCUMCurrentPositionRepository interface {
|
||||
Create(domain.SCUMCurrentPosition) error
|
||||
Get(string) (domain.SCUMCurrentPosition, error)
|
||||
List(domain.SCUMProjectionFilter) ([]domain.SCUMCurrentPosition, error)
|
||||
Update(domain.SCUMCurrentPosition) error
|
||||
}
|
||||
type SCUMOperationRequestRepository interface {
|
||||
Create(domain.SCUMOperationRequest) error
|
||||
Get(string) (domain.SCUMOperationRequest, error)
|
||||
List(domain.SCUMOperationRequestFilter) ([]domain.SCUMOperationRequest, error)
|
||||
Update(domain.SCUMOperationRequest) error
|
||||
}
|
||||
type SCUMWorkflowInstanceRepository interface {
|
||||
Create(domain.SCUMWorkflowInstance) error
|
||||
Get(string) (domain.SCUMWorkflowInstance, error)
|
||||
List(domain.SCUMWorkflowInstanceFilter) ([]domain.SCUMWorkflowInstance, error)
|
||||
Update(domain.SCUMWorkflowInstance) error
|
||||
}
|
||||
type SCUMWorkflowStepRepository interface {
|
||||
Create(domain.SCUMWorkflowStep) error
|
||||
Get(string) (domain.SCUMWorkflowStep, error)
|
||||
List(domain.SCUMWorkflowStepFilter) ([]domain.SCUMWorkflowStep, error)
|
||||
Update(domain.SCUMWorkflowStep) error
|
||||
}
|
||||
|
||||
type Store interface {
|
||||
Users() UserRepository
|
||||
AuthSessions() AuthSessionRepository
|
||||
@@ -336,6 +397,16 @@ type Store interface {
|
||||
GameGiftCatalogs() GameGiftCatalogRepository
|
||||
GameGiftRevisions() GameGiftRevisionRepository
|
||||
GameGiftGrants() GameGiftGrantRepository
|
||||
SCUMDataObservations() SCUMDataObservationRepository
|
||||
SCUMPlayerLiveStates() SCUMPlayerLiveStateRepository
|
||||
SCUMSquads() SCUMSquadRepository
|
||||
SCUMSquadMembers() SCUMSquadMemberRepository
|
||||
SCUMVehicles() SCUMVehicleRepository
|
||||
SCUMFlags() SCUMFlagRepository
|
||||
SCUMCurrentPositions() SCUMCurrentPositionRepository
|
||||
SCUMOperationRequests() SCUMOperationRequestRepository
|
||||
SCUMWorkflowInstances() SCUMWorkflowInstanceRepository
|
||||
SCUMWorkflowSteps() SCUMWorkflowStepRepository
|
||||
}
|
||||
|
||||
type MemoryStore struct {
|
||||
@@ -379,6 +450,16 @@ type MemoryStore struct {
|
||||
gameGiftCatalogs *memoryRepository[domain.GameGiftCatalog, domain.GameGiftCatalogFilter]
|
||||
gameGiftRevisions *memoryRepository[domain.GameGiftRevision, domain.GameGiftRevisionFilter]
|
||||
gameGiftGrants *memoryRepository[domain.GameGiftGrant, domain.GameGiftGrantFilter]
|
||||
scumDataObservations *memoryRepository[domain.SCUMDataObservation, domain.SCUMProjectionFilter]
|
||||
scumPlayerLiveStates *memoryRepository[domain.SCUMPlayerLiveState, domain.SCUMProjectionFilter]
|
||||
scumSquads *memoryRepository[domain.SCUMSquad, domain.SCUMProjectionFilter]
|
||||
scumSquadMembers *memoryRepository[domain.SCUMSquadMember, domain.SCUMProjectionFilter]
|
||||
scumVehicles *memoryRepository[domain.SCUMVehicle, domain.SCUMProjectionFilter]
|
||||
scumFlags *memoryRepository[domain.SCUMFlag, domain.SCUMProjectionFilter]
|
||||
scumCurrentPositions *memoryRepository[domain.SCUMCurrentPosition, domain.SCUMProjectionFilter]
|
||||
scumOperationRequests *memoryRepository[domain.SCUMOperationRequest, domain.SCUMOperationRequestFilter]
|
||||
scumWorkflowInstances *memoryRepository[domain.SCUMWorkflowInstance, domain.SCUMWorkflowInstanceFilter]
|
||||
scumWorkflowSteps *memoryRepository[domain.SCUMWorkflowStep, domain.SCUMWorkflowStepFilter]
|
||||
}
|
||||
|
||||
func NewMemoryStore() *MemoryStore {
|
||||
@@ -527,6 +608,16 @@ func NewMemoryStore() *MemoryStore {
|
||||
gameGiftCatalogs: newMemoryRepository(func(v domain.GameGiftCatalog) string { return v.ID }, domain.CopyGameGiftCatalog, matchGameGiftCatalog),
|
||||
gameGiftRevisions: newMemoryRepository(func(v domain.GameGiftRevision) string { return v.ID }, domain.CopyGameGiftRevision, matchGameGiftRevision),
|
||||
gameGiftGrants: newMemoryRepository(func(v domain.GameGiftGrant) string { return v.ID }, domain.CopyGameGiftGrant, matchGameGiftGrant),
|
||||
scumDataObservations: newMemoryRepository(func(v domain.SCUMDataObservation) string { return v.ID }, domain.CopySCUMDataObservation, matchSCUMDataObservation),
|
||||
scumPlayerLiveStates: newMemoryRepository(func(v domain.SCUMPlayerLiveState) string { return v.ID }, domain.CopySCUMPlayerLiveState, matchSCUMPlayerLiveState),
|
||||
scumSquads: newMemoryRepository(func(v domain.SCUMSquad) string { return v.ID }, domain.CopySCUMSquad, matchSCUMSquad),
|
||||
scumSquadMembers: newMemoryRepository(func(v domain.SCUMSquadMember) string { return v.ID }, domain.CopySCUMSquadMember, matchSCUMSquadMember),
|
||||
scumVehicles: newMemoryRepository(func(v domain.SCUMVehicle) string { return v.ID }, domain.CopySCUMVehicle, matchSCUMVehicle),
|
||||
scumFlags: newMemoryRepository(func(v domain.SCUMFlag) string { return v.ID }, domain.CopySCUMFlag, matchSCUMFlag),
|
||||
scumCurrentPositions: newMemoryRepository(func(v domain.SCUMCurrentPosition) string { return v.ID }, domain.CopySCUMCurrentPosition, matchSCUMCurrentPosition),
|
||||
scumOperationRequests: newMemoryRepository(func(v domain.SCUMOperationRequest) string { return v.ID }, domain.CopySCUMOperationRequest, matchSCUMOperationRequest),
|
||||
scumWorkflowInstances: newMemoryRepository(func(v domain.SCUMWorkflowInstance) string { return v.ID }, domain.CopySCUMWorkflowInstance, matchSCUMWorkflowInstance),
|
||||
scumWorkflowSteps: newMemoryRepository(func(v domain.SCUMWorkflowStep) string { return v.ID }, domain.CopySCUMWorkflowStep, matchSCUMWorkflowStep),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -606,6 +697,30 @@ func (store *MemoryStore) GameGiftRevisions() GameGiftRevisionRepository {
|
||||
return store.gameGiftRevisions
|
||||
}
|
||||
func (store *MemoryStore) GameGiftGrants() GameGiftGrantRepository { return store.gameGiftGrants }
|
||||
func (store *MemoryStore) SCUMDataObservations() SCUMDataObservationRepository {
|
||||
return store.scumDataObservations
|
||||
}
|
||||
func (store *MemoryStore) SCUMPlayerLiveStates() SCUMPlayerLiveStateRepository {
|
||||
return store.scumPlayerLiveStates
|
||||
}
|
||||
func (store *MemoryStore) SCUMSquads() SCUMSquadRepository { return store.scumSquads }
|
||||
func (store *MemoryStore) SCUMSquadMembers() SCUMSquadMemberRepository {
|
||||
return store.scumSquadMembers
|
||||
}
|
||||
func (store *MemoryStore) SCUMVehicles() SCUMVehicleRepository { return store.scumVehicles }
|
||||
func (store *MemoryStore) SCUMFlags() SCUMFlagRepository { return store.scumFlags }
|
||||
func (store *MemoryStore) SCUMCurrentPositions() SCUMCurrentPositionRepository {
|
||||
return store.scumCurrentPositions
|
||||
}
|
||||
func (store *MemoryStore) SCUMOperationRequests() SCUMOperationRequestRepository {
|
||||
return store.scumOperationRequests
|
||||
}
|
||||
func (store *MemoryStore) SCUMWorkflowInstances() SCUMWorkflowInstanceRepository {
|
||||
return store.scumWorkflowInstances
|
||||
}
|
||||
func (store *MemoryStore) SCUMWorkflowSteps() SCUMWorkflowStepRepository {
|
||||
return store.scumWorkflowSteps
|
||||
}
|
||||
|
||||
type memoryRepository[T any, F any] struct {
|
||||
mu sync.RWMutex
|
||||
@@ -940,3 +1055,100 @@ func matchGameGiftRevision(v domain.GameGiftRevision, f domain.GameGiftRevisionF
|
||||
func matchGameGiftGrant(v domain.GameGiftGrant, f domain.GameGiftGrantFilter) bool {
|
||||
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) && (f.GamePlayerRecordID == "" || v.GamePlayerRecordID == f.GamePlayerRecordID) && (f.IdempotencyKey == "" || v.IdempotencyKey == f.IdempotencyKey)
|
||||
}
|
||||
|
||||
func matchSCUMDataObservation(v domain.SCUMDataObservation, f domain.SCUMProjectionFilter) bool {
|
||||
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) &&
|
||||
(f.SubjectType == "" || v.SubjectType == string(f.SubjectType)) &&
|
||||
(f.GamePlayerRecordID == "" || v.SubjectID == f.GamePlayerRecordID) &&
|
||||
(f.QueryKey == "" || v.QueryKey == f.QueryKey) &&
|
||||
(f.Freshness == "" || domain.SCUMProjectionFreshness(v.Status) == f.Freshness)
|
||||
}
|
||||
|
||||
func matchSCUMPlayerLiveState(v domain.SCUMPlayerLiveState, f domain.SCUMProjectionFilter) bool {
|
||||
search := strings.ToLower(strings.TrimSpace(f.Search))
|
||||
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) &&
|
||||
(f.GamePlayerID == "" || v.GamePlayerID == f.GamePlayerID) &&
|
||||
(f.GamePlayerRecordID == "" || v.GamePlayerRecordID == f.GamePlayerRecordID) &&
|
||||
(f.UserProfileID == "" || v.UserProfileID == f.UserProfileID) &&
|
||||
(f.SteamID == "" || v.SteamID == f.SteamID) &&
|
||||
(f.SquadID == "" || v.SquadID == f.SquadID) &&
|
||||
(f.Freshness == "" || v.Freshness.Status == f.Freshness) &&
|
||||
(search == "" || strings.Contains(strings.ToLower(v.DisplayName), search) || strings.Contains(strings.ToLower(v.GamePlayerID), search) || strings.Contains(strings.ToLower(v.UserProfileID), search) || strings.Contains(strings.ToLower(v.SteamID), search))
|
||||
}
|
||||
|
||||
func matchSCUMSquad(v domain.SCUMSquad, f domain.SCUMProjectionFilter) bool {
|
||||
search := strings.ToLower(strings.TrimSpace(f.Search))
|
||||
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) &&
|
||||
(f.SquadID == "" || v.SquadID == f.SquadID) &&
|
||||
(f.UserProfileID == "" || v.LeaderProfileID == f.UserProfileID) &&
|
||||
(f.Freshness == "" || v.Freshness.Status == f.Freshness) &&
|
||||
(search == "" || strings.Contains(strings.ToLower(v.Name), search) || strings.Contains(strings.ToLower(v.SquadID), search))
|
||||
}
|
||||
|
||||
func matchSCUMSquadMember(v domain.SCUMSquadMember, f domain.SCUMProjectionFilter) bool {
|
||||
search := strings.ToLower(strings.TrimSpace(f.Search))
|
||||
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) &&
|
||||
(f.SquadID == "" || v.SquadID == f.SquadID) &&
|
||||
(f.GamePlayerID == "" || v.GamePlayerID == f.GamePlayerID) &&
|
||||
(f.GamePlayerRecordID == "" || v.GamePlayerRecordID == f.GamePlayerRecordID) &&
|
||||
(f.UserProfileID == "" || v.UserProfileID == f.UserProfileID) &&
|
||||
(f.SteamID == "" || v.SteamID == f.SteamID) &&
|
||||
(f.Freshness == "" || v.Freshness.Status == f.Freshness) &&
|
||||
(search == "" || strings.Contains(strings.ToLower(v.DisplayName), search) || strings.Contains(strings.ToLower(v.GamePlayerID), search) || strings.Contains(strings.ToLower(v.UserProfileID), search))
|
||||
}
|
||||
|
||||
func matchSCUMVehicle(v domain.SCUMVehicle, f domain.SCUMProjectionFilter) bool {
|
||||
search := strings.ToLower(strings.TrimSpace(f.Search))
|
||||
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) &&
|
||||
(f.VehicleID == "" || v.VehicleID == f.VehicleID) &&
|
||||
(f.UserProfileID == "" || v.OwnerProfileID == f.UserProfileID) &&
|
||||
(f.GamePlayerID == "" || v.OwnerPlayerID == f.GamePlayerID) &&
|
||||
(f.SquadID == "" || v.SquadID == f.SquadID) &&
|
||||
(f.Freshness == "" || v.Freshness.Status == f.Freshness) &&
|
||||
(search == "" || strings.Contains(strings.ToLower(v.Label), search) || strings.Contains(strings.ToLower(v.ClassName), search) || strings.Contains(strings.ToLower(v.VehicleID), search))
|
||||
}
|
||||
|
||||
func matchSCUMFlag(v domain.SCUMFlag, f domain.SCUMProjectionFilter) bool {
|
||||
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) &&
|
||||
(f.FlagID == "" || v.FlagID == f.FlagID) &&
|
||||
(f.UserProfileID == "" || v.OwnerProfileID == f.UserProfileID) &&
|
||||
(f.GamePlayerID == "" || v.OwnerPlayerID == f.GamePlayerID) &&
|
||||
(f.SquadID == "" || v.OwnerSquadID == f.SquadID) &&
|
||||
(f.Freshness == "" || v.Freshness.Status == f.Freshness)
|
||||
}
|
||||
|
||||
func matchSCUMCurrentPosition(v domain.SCUMCurrentPosition, f domain.SCUMProjectionFilter) bool {
|
||||
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) &&
|
||||
(f.SubjectType == "" || v.SubjectType == f.SubjectType) &&
|
||||
(f.GamePlayerID == "" || v.GamePlayerID == f.GamePlayerID) &&
|
||||
(f.GamePlayerRecordID == "" || v.GamePlayerRecordID == f.GamePlayerRecordID) &&
|
||||
(f.VehicleID == "" || v.VehicleID == f.VehicleID) &&
|
||||
(f.Freshness == "" || v.Freshness.Status == f.Freshness)
|
||||
}
|
||||
|
||||
func matchSCUMOperationRequest(v domain.SCUMOperationRequest, f domain.SCUMOperationRequestFilter) bool {
|
||||
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) &&
|
||||
(f.PluginID == "" || v.PluginID == f.PluginID) &&
|
||||
(f.TemplateKey == "" || v.TemplateKey == f.TemplateKey) &&
|
||||
(f.PlayerID == "" || v.PlayerID == f.PlayerID) &&
|
||||
(f.RequesterID == "" || v.RequesterID == f.RequesterID) &&
|
||||
(f.Status == "" || v.Status == f.Status) &&
|
||||
(f.IdempotencyKey == "" || v.IdempotencyKey == f.IdempotencyKey)
|
||||
}
|
||||
|
||||
func matchSCUMWorkflowInstance(v domain.SCUMWorkflowInstance, f domain.SCUMWorkflowInstanceFilter) bool {
|
||||
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) &&
|
||||
(f.PluginID == "" || v.PluginID == f.PluginID) &&
|
||||
(f.TemplateKey == "" || v.TemplateKey == f.TemplateKey) &&
|
||||
(f.RequestedBy == "" || v.RequestedBy == f.RequestedBy) &&
|
||||
(f.Status == "" || v.Status == f.Status) &&
|
||||
(f.IdempotencyKey == "" || v.IdempotencyKey == f.IdempotencyKey)
|
||||
}
|
||||
|
||||
func matchSCUMWorkflowStep(v domain.SCUMWorkflowStep, f domain.SCUMWorkflowStepFilter) bool {
|
||||
return (f.WorkflowID == "" || v.WorkflowID == f.WorkflowID) &&
|
||||
(f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) &&
|
||||
(f.StepKey == "" || v.StepKey == f.StepKey) &&
|
||||
(f.Status == "" || v.Status == f.Status) &&
|
||||
(f.MutatesState == nil || v.MutatesState == *f.MutatesState)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
func TestSCUMProjectionRepositoriesCopyFilterAndPersist(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "metadata.json")
|
||||
store, err := NewFileStore(path)
|
||||
if err != nil {
|
||||
t.Fatalf("create file store: %v", err)
|
||||
}
|
||||
stamp := time.Date(2026, 8, 10, 9, 0, 0, 0, time.UTC)
|
||||
freshness := domain.SCUMProjectionFreshnessState{Status: domain.SCUMProjectionFresh, ObservationID: "obs-1", Source: "run", QueryKey: "scum.player.profile", Sequence: 7, Checksum: "sha256:projection", ObservedAt: stamp, ReceivedAt: stamp.Add(time.Second)}
|
||||
state := domain.SCUMPlayerLiveState{ID: "state-1", ServerInstanceID: "server-1", GamePlayerRecordID: "game-player-1", GamePlayerID: "steam-1", UserProfileID: "profile-1", SteamID: "steam-1", DisplayName: "Moon", SquadID: "squad-1", UnknownFields: map[string]any{"schemaField": "kept"}, Freshness: freshness, CreatedAt: stamp, UpdatedAt: stamp}
|
||||
if err := store.SCUMPlayerLiveStates().Create(state); err != nil {
|
||||
t.Fatalf("create state: %v", err)
|
||||
}
|
||||
got, err := store.SCUMPlayerLiveStates().Get(state.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get state: %v", err)
|
||||
}
|
||||
got.UnknownFields["schemaField"] = "mutated"
|
||||
again, err := store.SCUMPlayerLiveStates().Get(state.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get state again: %v", err)
|
||||
}
|
||||
if again.UnknownFields["schemaField"] != "kept" {
|
||||
t.Fatalf("state was not copy-isolated: %+v", again.UnknownFields)
|
||||
}
|
||||
filtered, err := store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", UserProfileID: "profile-1", Search: "moon"})
|
||||
if err != nil || len(filtered) != 1 {
|
||||
t.Fatalf("filter states=%+v err=%v", filtered, err)
|
||||
}
|
||||
if err := store.SCUMSquads().Create(domain.SCUMSquad{ID: "squad-1", ServerInstanceID: "server-1", SquadID: "squad-1", Name: "Crystal", Freshness: freshness}); err != nil {
|
||||
t.Fatalf("create squad: %v", err)
|
||||
}
|
||||
if err := store.SCUMVehicles().Create(domain.SCUMVehicle{ID: "vehicle-1", ServerInstanceID: "server-1", VehicleID: "veh-1", Label: "Unknown vehicle", Freshness: freshness}); err != nil {
|
||||
t.Fatalf("create vehicle: %v", err)
|
||||
}
|
||||
if err := store.SCUMFlags().Create(domain.SCUMFlag{ID: "flag-1", ServerInstanceID: "server-1", FlagID: "flag-1", OwnerSquadID: "squad-1", Freshness: freshness}); err != nil {
|
||||
t.Fatalf("create flag: %v", err)
|
||||
}
|
||||
if err := store.SCUMCurrentPositions().Create(domain.SCUMCurrentPosition{ID: "position-1", ServerInstanceID: "server-1", SubjectType: domain.SCUMProjectionSubjectPlayer, SubjectID: "steam-1", GamePlayerRecordID: "game-player-1", X: 1, Y: 2, HasCoordinates: true, Freshness: freshness}); err != nil {
|
||||
t.Fatalf("create position: %v", err)
|
||||
}
|
||||
reloaded, err := NewFileStore(path)
|
||||
if err != nil {
|
||||
t.Fatalf("reload file store: %v", err)
|
||||
}
|
||||
reloadedStates, err := reloaded.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", SquadID: "squad-1"})
|
||||
if err != nil || len(reloadedStates) != 1 || reloadedStates[0].Freshness.QueryKey != "scum.player.profile" {
|
||||
t.Fatalf("unexpected reloaded states=%+v err=%v", reloadedStates, err)
|
||||
}
|
||||
vehicles, err := reloaded.SCUMVehicles().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", Search: "unknown"})
|
||||
if err != nil || len(vehicles) != 1 {
|
||||
t.Fatalf("unexpected reloaded vehicles=%+v err=%v", vehicles, err)
|
||||
}
|
||||
positions, err := reloaded.SCUMCurrentPositions().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", SubjectType: domain.SCUMProjectionSubjectPlayer})
|
||||
if err != nil || len(positions) != 1 || !positions[0].HasCoordinates {
|
||||
t.Fatalf("unexpected reloaded positions=%+v err=%v", positions, err)
|
||||
}
|
||||
}
|
||||
@@ -268,11 +268,6 @@ func runAutonomousLifecyclePlan(distribution domain.RunDistribution, instance do
|
||||
plan.LogSources = append(plan.LogSources, autonomousLogSource(source))
|
||||
}
|
||||
}
|
||||
for _, target := range plugin.RuntimeProfiles.DataTargets {
|
||||
if runtimePlatformsContain(target.Platforms, distribution.TargetOS) {
|
||||
plan.DataTargets = append(plan.DataTargets, autonomousDataTarget(target))
|
||||
}
|
||||
}
|
||||
if hasProfile && len(profile.DLLExtensionRefs) > 0 {
|
||||
endpoint := domain.RunEndpoint{ID: distribution.RunEndpointID, Platform: distribution.TargetOS, Architecture: distribution.TargetArch}
|
||||
extensions, err := lifecycleDLLExtensionPlans(plugin.RuntimeProfiles, profile, endpoint)
|
||||
@@ -331,10 +326,6 @@ func autonomousDLLExtension(extension domain.RuntimeDLLExtensionPlan) domain.Run
|
||||
return domain.RunAutonomousDLLExtension{Key: extension.Key, Version: extension.Version, ReleaseURL: extension.ReleaseURL, Checksum: extension.Checksum, SizeBytes: extension.SizeBytes, TargetKey: extension.TargetKey, ModKey: extension.ModKey, DLLRef: extension.DLLRef, SCUMExecutableChecksum: extension.SCUMExecutableChecksum, UE4SSABI: extension.UE4SSABI, RCONPort: extension.RCONPort}
|
||||
}
|
||||
|
||||
func autonomousDataTarget(target domain.RuntimeDataTarget) domain.RunAutonomousDataTarget {
|
||||
return domain.RunAutonomousDataTarget{Key: target.Key, Kind: target.Kind, TransportKey: target.TransportKey, SourceRootKey: target.SourceRootKey, SourcePath: target.SourcePath, WorkspaceKey: target.WorkspaceKey, RefreshPolicy: target.RefreshPolicy, MaxBytes: target.MaxBytes, Platforms: domain.CopyStringSlice(target.Platforms)}
|
||||
}
|
||||
|
||||
func autonomousDeploymentFromDefinition(definition domain.ServerDeploymentDefinition, profileKey string, bindings map[string]string) *domain.RunAutonomousDeployment {
|
||||
if definition.Mode == "" {
|
||||
return nil
|
||||
|
||||
@@ -45,13 +45,6 @@ func TestCoreServiceKeepsPlatformBuildKeyOffMachineJobChannel(t *testing.T) {
|
||||
domain.RuntimeLogSource{Key: "console", Kind: "process.stdout", TargetKey: "server/process", StreamKey: "console", CursorKind: "sequence", RetentionDays: 14},
|
||||
domain.RuntimeLogSource{Key: "server-events", Kind: "file.tail", TargetKey: "logs/server", StreamKey: "scum.server", CursorKind: "fingerprint", RetentionDays: 90},
|
||||
)
|
||||
plugin.RuntimeProfiles.TransportProfiles = append(plugin.RuntimeProfiles.TransportProfiles,
|
||||
domain.RuntimeTransportProfile{Key: "server-files", Kind: "file", TargetKey: "server-root", Capabilities: []string{domain.JobCapabilityRemoteRunFilesRead}},
|
||||
domain.RuntimeTransportProfile{Key: "world-db", Kind: "sqlite", TargetKey: "world-db", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}},
|
||||
)
|
||||
plugin.RuntimeProfiles.DataTargets = append(plugin.RuntimeProfiles.DataTargets,
|
||||
domain.RuntimeDataTarget{Key: "world-db", Kind: "sqlite.snapshot", TransportKey: "world-db", SourceRootKey: "server-root", SourcePath: "world/current.db", WorkspaceKey: "databases/world-db", RefreshPolicy: "on-demand-snapshot", MaxBytes: 128 * 1024 * 1024, Platforms: []string{"linux"}},
|
||||
)
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatalf("seed plugin lifecycle assets: %v", err)
|
||||
}
|
||||
@@ -97,14 +90,14 @@ func TestCoreServiceKeepsPlatformBuildKeyOffMachineJobChannel(t *testing.T) {
|
||||
if plan == nil || plan.SchemaVersion != "1" || plan.ServerInstanceID != instance.ID || plan.PluginID != plugin.ID || plan.ProfileKey != "local" || plan.Bootstrap == nil || plan.Bootstrap.Action != domain.ServerLifecycleActionStart || plan.Bootstrap.TargetKey != "actions/start.json" {
|
||||
t.Fatalf("platform builder received incomplete autonomous lifecycle plan: %+v", plan)
|
||||
}
|
||||
if len(plan.DependencyProbes) != 1 || plan.DependencyProbes[0].Key != "java-runtime" || len(plan.InstallPlans) != 1 || plan.InstallPlans[0].Key != "java-install" || len(plan.LogSources) != 3 || !hasAutonomousLogSource(plan.LogSources, "process.stdout", "console") || !hasAutonomousLogSource(plan.LogSources, "file.tail", "latest-log") || !hasAutonomousLogSource(plan.LogSources, "file.tail", "scum.server") || len(plan.DataTargets) != 1 || plan.DataTargets[0].WorkspaceKey != "databases/world-db" || plan.DataTargets[0].SourcePath != "world/current.db" || plan.RuntimeBindings["logs/latest"] != "runtime.logs.latest" {
|
||||
if len(plan.DependencyProbes) != 1 || plan.DependencyProbes[0].Key != "java-runtime" || len(plan.InstallPlans) != 1 || plan.InstallPlans[0].Key != "java-install" || len(plan.LogSources) != 3 || !hasAutonomousLogSource(plan.LogSources, "process.stdout", "console") || !hasAutonomousLogSource(plan.LogSources, "file.tail", "latest-log") || !hasAutonomousLogSource(plan.LogSources, "file.tail", "scum.server") || plan.RuntimeBindings["logs/latest"] != "runtime.logs.latest" {
|
||||
t.Fatalf("autonomous lifecycle plan lost plugin runtime declarations: %+v", plan)
|
||||
}
|
||||
var seededPlan domain.RunAutonomousLifecyclePlan
|
||||
if err := json.Unmarshal([]byte(seedFiles[2].Content), &seededPlan); err != nil {
|
||||
t.Fatalf("unmarshal seeded autonomous lifecycle plan: %v", err)
|
||||
}
|
||||
if seededPlan.ServerInstanceID != plan.ServerInstanceID || seededPlan.Bootstrap == nil || seededPlan.Bootstrap.TargetKey != plan.Bootstrap.TargetKey || len(seededPlan.DataTargets) != 1 || seededPlan.DataTargets[0].WorkspaceKey != "databases/world-db" {
|
||||
if seededPlan.ServerInstanceID != plan.ServerInstanceID || seededPlan.Bootstrap == nil || seededPlan.Bootstrap.TargetKey != plan.Bootstrap.TargetKey {
|
||||
t.Fatalf("seeded lifecycle plan differs from build input: seed=%+v input=%+v", seededPlan, plan)
|
||||
}
|
||||
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
|
||||
|
||||
@@ -764,7 +764,7 @@ func (svc *CoreService) QueueLogBackfillForSession(sessionID string, request dom
|
||||
InputRef: request.CheckpointRef,
|
||||
IdempotencyKey: request.IdempotencyKey,
|
||||
Progress: domain.JobProgress{Percent: 0, Message: "historical log backfill queued"},
|
||||
ExecutionInput: domain.JobExecutionInput{PluginID: plugin.ID, TargetVersion: plugin.Version, LogSource: &source},
|
||||
ExecutionInput: domain.JobExecutionInput{LogSource: &source},
|
||||
})
|
||||
if err != nil {
|
||||
_ = svc.recordAuditEvent(user.ID, "logs.backfill.denied", "server-instance", instance.ID, domain.AuditResultDenied, "log backfill denied: endpoint unsupported or offline")
|
||||
|
||||
@@ -125,6 +125,9 @@ func (svc *CoreService) projectGamePlayerEvent(batch domain.LogBatchIngest, entr
|
||||
if eventType == "scum.login" {
|
||||
outcome := strings.TrimSpace(fields["outcome"])
|
||||
if outcome == "accepted" {
|
||||
if err := svc.projectSCUMLoginLiveState(player, batch, entry, occurred, true, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := svc.recordSuccessfulGameAccess(player, batch, entry, occurred, strings.TrimSpace(fields["networkFingerprint"])); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -132,6 +135,9 @@ func (svc *CoreService) projectGamePlayerEvent(batch domain.LogBatchIngest, entr
|
||||
}
|
||||
return svc.recordFailedGameAccess(player, batch, entry, occurred, strings.TrimSpace(fields["networkFingerprint"]))
|
||||
}
|
||||
if err := svc.projectSCUMLoginLiveState(player, batch, entry, occurred, false, strings.TrimSpace(fields["reason"])); err != nil {
|
||||
return err
|
||||
}
|
||||
return svc.closeGamePlayerSession(player, sourceSession, occurred, strings.TrimSpace(fields["reason"]))
|
||||
}
|
||||
|
||||
|
||||
@@ -288,9 +288,6 @@ func (svc *CoreService) CompleteRunJob(result domain.RunJobResult) (domain.RunJo
|
||||
}
|
||||
|
||||
func validateExecutionResultForJob(job domain.Job, result domain.RunJobResult) error {
|
||||
if result.ExecutionResult.ParsedLogBatch != nil && job.Capability != domain.JobCapabilityLogsBackfill {
|
||||
return validationError("parsed log batch result is allowed only for logs.backfill jobs")
|
||||
}
|
||||
if definition := job.ExecutionInput.Deployment; definition != nil {
|
||||
receipt := result.ExecutionResult.DeploymentReceipt
|
||||
if result.State == domain.JobStateSucceeded && definition.Mode == domain.ServerDeploymentModeCustom && receipt == nil {
|
||||
@@ -337,72 +334,6 @@ func validateExecutionResultForJob(job domain.Job, result domain.RunJobResult) e
|
||||
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "run.update.staged" {
|
||||
return validationError("Run self-update result type is invalid")
|
||||
}
|
||||
case domain.JobCapabilityRemoteRunDBSQLiteProbe:
|
||||
if result.ExecutionResult.Kind != "" && result.ExecutionResult.Kind != scumSchemaProbeExecutionKind {
|
||||
return validationError("SQLite schema probe result type is invalid")
|
||||
}
|
||||
if result.State == domain.JobStateSucceeded {
|
||||
if result.ExecutionResult.SQLiteSchemaProbe == nil {
|
||||
return validationError("SQLite schema probe terminal result is required")
|
||||
}
|
||||
if err := validateSCUMSchemaProbeResultForJob(job, *result.ExecutionResult.SQLiteSchemaProbe); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case domain.JobCapabilityRemoteRunDBSQLiteQuery:
|
||||
if result.ExecutionResult.Kind != "" && result.ExecutionResult.Kind != scumSQLiteTemplateExecutionKind {
|
||||
return validationError("SQLite template query result type is invalid")
|
||||
}
|
||||
if result.State == domain.JobStateSucceeded {
|
||||
if result.ExecutionResult.SQLiteTemplate == nil {
|
||||
return validationError("SQLite template terminal result is required")
|
||||
}
|
||||
if err := validateSCUMSQLiteTemplateResultForJob(job, *result.ExecutionResult.SQLiteTemplate); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case domain.JobCapabilityRemoteRunProtectedRCON:
|
||||
if job.ExecutionInput.RCONTemplate == nil {
|
||||
break
|
||||
}
|
||||
if result.ExecutionResult.Kind != "" && result.ExecutionResult.Kind != scumRCONTemplateExecutionKind {
|
||||
return validationError("typed RCON template result type is invalid")
|
||||
}
|
||||
if result.State == domain.JobStateSucceeded {
|
||||
if result.ExecutionResult.RCONTemplate == nil {
|
||||
return validationError("typed RCON template terminal result is required")
|
||||
}
|
||||
if err := validateSCUMTypedRCONTemplateResultForJob(job, *result.ExecutionResult.RCONTemplate); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case domain.JobCapabilityRemoteRunProtectedSQL:
|
||||
if job.ExecutionInput.GuardedMutation == nil {
|
||||
break
|
||||
}
|
||||
if result.ExecutionResult.Kind != "" && result.ExecutionResult.Kind != scumGuardedMutationExecutionKind {
|
||||
return validationError("guarded mutation result type is invalid")
|
||||
}
|
||||
if result.State == domain.JobStateSucceeded {
|
||||
if result.ExecutionResult.GuardedMutation == nil {
|
||||
return validationError("guarded mutation terminal result is required")
|
||||
}
|
||||
if err := validateSCUMGuardedMutationResultForJob(job, *result.ExecutionResult.GuardedMutation); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case domain.JobCapabilityLogsBackfill:
|
||||
if result.ExecutionResult.ParsedLogBatch == nil {
|
||||
break
|
||||
}
|
||||
if result.ExecutionResult.Kind != "" && result.ExecutionResult.Kind != scumParsedLogBatchExecutionKind {
|
||||
return validationError("parsed log batch result type is invalid")
|
||||
}
|
||||
if result.State == domain.JobStateSucceeded {
|
||||
if err := validateSCUMParsedLogBatchResultForJob(job, *result.ExecutionResult.ParsedLogBatch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case domain.JobCapabilityClientManagerDeploy:
|
||||
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "client-manager.deployed" {
|
||||
return validationError("client-manager deploy result type is invalid")
|
||||
@@ -694,7 +625,7 @@ func firstEligibleSupportedJob(jobs []domain.Job, capabilities []string, stamp t
|
||||
|
||||
func assignmentFromJob(job domain.Job, leaseToken string) domain.RunJobAssignment {
|
||||
fencingToken := uint64(0)
|
||||
if isProtectedRequestCapability(job.Capability) || job.Capability == domain.JobCapabilityRemoteRunDBSQLiteProbe || job.Capability == domain.JobCapabilityRemoteRunDBSQLiteQuery {
|
||||
if isProtectedRequestCapability(job.Capability) {
|
||||
fencingToken = uint64(job.Attempt)
|
||||
}
|
||||
return domain.RunJobAssignment{
|
||||
@@ -708,7 +639,7 @@ func assignmentFromJob(job domain.Job, leaseToken string) domain.RunJobAssignmen
|
||||
State: job.State,
|
||||
Progress: domain.RunJobProgressReport{Percent: job.Progress.Percent, Phase: job.Progress.Phase, Message: job.Progress.Message},
|
||||
ResultRef: job.ResultRef,
|
||||
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: job.ExecutionInput.WorkspaceScope, Content: job.ExecutionInput.Content, ExpectedVersion: job.ExecutionInput.ExpectedVersion, ExpectedChecksum: job.ExecutionInput.ExpectedChecksum, MaxReadBytes: job.ExecutionInput.MaxReadBytes, RemoteAdapterKey: job.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: job.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: job.ExecutionInput.TimeoutSeconds, PluginID: job.ExecutionInput.PluginID, LifecycleOperation: job.ExecutionInput.LifecycleOperation, TargetVersion: job.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(job.ExecutionInput.Inputs), LogSource: domain.CopyRuntimeLogSourcePtr(job.ExecutionInput.LogSource), LogSources: domain.CopyRuntimeLogSources(job.ExecutionInput.LogSources), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), job.ExecutionInput.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(job.ExecutionInput.SourceRCON), Deployment: deploymentPlanForDispatchValue(job.ExecutionInput.Deployment), ServerDeploymentPlan: domain.CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan), SQLiteSchemaProbe: domain.CopySCUMSchemaProbeRequestPtr(job.ExecutionInput.SQLiteSchemaProbe), SQLiteTemplate: domain.CopySCUMSQLiteTemplateRequestPtr(job.ExecutionInput.SQLiteTemplate), RCONTemplate: domain.CopySCUMTypedRCONTemplateRequestPtr(job.ExecutionInput.RCONTemplate), GuardedMutation: domain.CopySCUMGuardedMutationRequestPtr(job.ExecutionInput.GuardedMutation)},
|
||||
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: job.ExecutionInput.WorkspaceScope, Content: job.ExecutionInput.Content, ExpectedVersion: job.ExecutionInput.ExpectedVersion, ExpectedChecksum: job.ExecutionInput.ExpectedChecksum, MaxReadBytes: job.ExecutionInput.MaxReadBytes, RemoteAdapterKey: job.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: job.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: job.ExecutionInput.TimeoutSeconds, PluginID: job.ExecutionInput.PluginID, LifecycleOperation: job.ExecutionInput.LifecycleOperation, TargetVersion: job.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(job.ExecutionInput.Inputs), LogSource: domain.CopyRuntimeLogSourcePtr(job.ExecutionInput.LogSource), LogSources: domain.CopyRuntimeLogSources(job.ExecutionInput.LogSources), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), job.ExecutionInput.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(job.ExecutionInput.SourceRCON), Deployment: deploymentPlanForDispatchValue(job.ExecutionInput.Deployment), ServerDeploymentPlan: domain.CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan)},
|
||||
LeaseToken: leaseToken,
|
||||
Attempt: job.Attempt,
|
||||
FencingToken: fencingToken,
|
||||
@@ -786,27 +717,7 @@ func jobRetryBackoff(policy domain.JobRetryPolicy, attempt int) time.Duration {
|
||||
}
|
||||
|
||||
func terminalFingerprint(result domain.RunJobResult) string {
|
||||
schemaProbeFingerprint := ""
|
||||
if result.ExecutionResult.SQLiteSchemaProbe != nil {
|
||||
schemaProbeFingerprint = result.ExecutionResult.SQLiteSchemaProbe.ResultDigest
|
||||
}
|
||||
sqliteTemplateFingerprint := ""
|
||||
if result.ExecutionResult.SQLiteTemplate != nil {
|
||||
sqliteTemplateFingerprint = fmt.Sprintf("%s|%s|%s|%d", result.ExecutionResult.SQLiteTemplate.ResultDigest, result.ExecutionResult.SQLiteTemplate.AssetDigest, result.ExecutionResult.SQLiteTemplate.ParameterDigest, result.ExecutionResult.SQLiteTemplate.RowCount)
|
||||
}
|
||||
rconTemplateFingerprint := ""
|
||||
if result.ExecutionResult.RCONTemplate != nil {
|
||||
rconTemplateFingerprint = fmt.Sprintf("%s|%s|%s|%s|%s", result.ExecutionResult.RCONTemplate.ResultDigest, result.ExecutionResult.RCONTemplate.AssetDigest, result.ExecutionResult.RCONTemplate.PayloadDigest, result.ExecutionResult.RCONTemplate.ConfirmationDigest, result.ExecutionResult.RCONTemplate.ConfirmationStatus)
|
||||
}
|
||||
guardedMutationFingerprint := ""
|
||||
if result.ExecutionResult.GuardedMutation != nil {
|
||||
guardedMutationFingerprint = fmt.Sprintf("%s|%s|%s|%d|%s", result.ExecutionResult.GuardedMutation.ResultDigest, result.ExecutionResult.GuardedMutation.AssetDigest, result.ExecutionResult.GuardedMutation.PatchDigest, result.ExecutionResult.GuardedMutation.AffectedRows, result.ExecutionResult.GuardedMutation.ReadbackStatus)
|
||||
}
|
||||
parsedLogBatchFingerprint := ""
|
||||
if result.ExecutionResult.ParsedLogBatch != nil {
|
||||
parsedLogBatchFingerprint = fmt.Sprintf("%s|%s|%s|%s|%d|%s", result.ExecutionResult.ParsedLogBatch.ResultDigest, result.ExecutionResult.ParsedLogBatch.AssetDigest, result.ExecutionResult.ParsedLogBatch.ParserDigest, result.ExecutionResult.ParsedLogBatch.FirstCursor.StreamGeneration, result.ExecutionResult.ParsedLogBatch.EventCount, result.ExecutionResult.ParsedLogBatch.TailState)
|
||||
}
|
||||
return fmt.Sprintf("%s|%d|%s|%s|%s|%s|%t|%s|%s|%s|%s|%s|%s|%s", result.State, result.Progress.Percent, result.ResultRef, result.Message, result.ErrorCode, result.Progress.Message, result.Retryable, result.ExecutionResult.Kind, result.ExecutionResult.Checksum, schemaProbeFingerprint, sqliteTemplateFingerprint, rconTemplateFingerprint, guardedMutationFingerprint, parsedLogBatchFingerprint)
|
||||
return fmt.Sprintf("%s|%d|%s|%s|%s|%s|%t", result.State, result.Progress.Percent, result.ResultRef, result.Message, result.ErrorCode, result.Progress.Message, result.Retryable)
|
||||
}
|
||||
|
||||
func terminalMessage(result domain.RunJobResult) string {
|
||||
|
||||
@@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
@@ -213,147 +212,6 @@ func TestCoreServiceRunJobTerminalResultIsIdempotent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRunJobSQLiteTemplateEnvelopeIsFencedToLease(t *testing.T) {
|
||||
svc, sessionToken := newRegisteredRunJobService(t)
|
||||
request := domain.SCUMSQLiteTemplateRequest{RequestID: "request-query", JobID: "job-query", Binding: scumTemplateTestBinding(), Capability: domain.SCUMDataCapabilityPlayerRead, TargetKey: "scum-database", TemplateKey: "players.active.v1", AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumTemplateTestHash(), AssetDigest: scumTemplateTestHash(), ParameterDigest: scumTemplateTestHash(), Parameters: map[string]any{"limit": 100.0}, Bounds: domain.DefaultSCUMSQLiteTemplateBounds(), RequestedAt: time.Now()}
|
||||
createSCUMTemplateServerFixture(t, svc, request.Binding)
|
||||
job, err := svc.CreateJob(domain.Job{ID: request.JobID, ServerInstanceID: request.Binding.ServerInstanceID, RunEndpointID: request.Binding.RunEndpointID, Capability: domain.JobCapabilityRemoteRunDBSQLiteQuery, TargetKey: request.TargetKey, InputRef: "input://sqlite-template/request-query", IdempotencyKey: "idem-query", ExecutionInput: domain.JobExecutionInput{SQLiteTemplate: &request}})
|
||||
if err != nil {
|
||||
t.Fatalf("create sqlite query job: %v", err)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: request.Binding.RunEndpointID, SessionToken: sessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||
if err != nil || !claim.HasJob || claim.Job.JobID != job.ID || claim.Job.ExecutionInput.SQLiteTemplate == nil {
|
||||
t.Fatalf("claim sqlite query job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
result := scumTemplateTestResult(request)
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: request.Binding.RunEndpointID, SessionToken: sessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "query complete"}, Message: "query complete", ExecutionResult: domain.JobExecutionResult{Kind: scumSQLiteTemplateExecutionKind, SQLiteTemplate: &result, AuditSummary: "redacted sqlite template query"}}); err != nil {
|
||||
t.Fatalf("complete matching sqlite query: %v", err)
|
||||
}
|
||||
|
||||
badRequest := request
|
||||
badRequest.RequestID = "request-query-bad"
|
||||
badRequest.JobID = "job-query-bad"
|
||||
if _, err := svc.CreateJob(domain.Job{ID: badRequest.JobID, ServerInstanceID: badRequest.Binding.ServerInstanceID, RunEndpointID: badRequest.Binding.RunEndpointID, Capability: domain.JobCapabilityRemoteRunDBSQLiteQuery, TargetKey: badRequest.TargetKey, InputRef: "input://sqlite-template/request-query-bad", IdempotencyKey: "idem-query-bad", ExecutionInput: domain.JobExecutionInput{SQLiteTemplate: &badRequest}}); err != nil {
|
||||
t.Fatalf("create bad sqlite query job: %v", err)
|
||||
}
|
||||
badClaim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: badRequest.Binding.RunEndpointID, SessionToken: sessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||
if err != nil {
|
||||
t.Fatalf("claim bad sqlite query job: %v", err)
|
||||
}
|
||||
badResult := scumTemplateTestResult(badRequest)
|
||||
badResult.AssetDigest = "sha256:" + strings.Repeat("b", 64)
|
||||
_, err = svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: badRequest.Binding.RunEndpointID, SessionToken: sessionToken, JobID: badClaim.Job.JobID, LeaseToken: badClaim.Job.LeaseToken, Attempt: badClaim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "query complete"}, Message: "query complete", ExecutionResult: domain.JobExecutionResult{Kind: scumSQLiteTemplateExecutionKind, SQLiteTemplate: &badResult, AuditSummary: "redacted sqlite template query"}})
|
||||
if err == nil || !strings.Contains(err.Error(), "digest") {
|
||||
t.Fatalf("expected digest-fenced query result rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRunJobTypedRCONTemplateEnvelopeIsFencedToLease(t *testing.T) {
|
||||
svc, sessionToken := newRegisteredRunJobService(t)
|
||||
request := scumTypedRCONTemplateTestRequest()
|
||||
createSCUMTemplateServerFixture(t, svc, request.Binding)
|
||||
job, err := svc.CreateJob(domain.Job{ID: request.JobID, ServerInstanceID: request.Binding.ServerInstanceID, RunEndpointID: request.Binding.RunEndpointID, Capability: domain.JobCapabilityRemoteRunProtectedRCON, TargetKey: request.TargetKey, InputRef: "input://rcon-template/request-rcon", IdempotencyKey: request.IdempotencyKey, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, ExecutionInput: domain.JobExecutionInput{RemoteAdapterKind: "protected-rcon", SourceRCON: scumTypedRCONSourcePlan(), RCONTemplate: &request}})
|
||||
if err != nil {
|
||||
t.Fatalf("create typed RCON job: %v", err)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: request.Binding.RunEndpointID, SessionToken: sessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunProtectedRCON}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||
if err != nil || !claim.HasJob || claim.Job.JobID != job.ID || claim.Job.ExecutionInput.RCONTemplate == nil || claim.Job.ExecutionInput.RCONTemplate.Payload["absoluteValue"].(float64) != 100.0 {
|
||||
t.Fatalf("claim typed RCON job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
result := scumTypedRCONTemplateTestResult(request)
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: request.Binding.RunEndpointID, SessionToken: sessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "rcon complete"}, Message: "rcon complete", ExecutionResult: domain.JobExecutionResult{Kind: scumRCONTemplateExecutionKind, RCONTemplate: &result, AuditSummary: "redacted typed RCON template command"}}); err != nil {
|
||||
t.Fatalf("complete matching typed RCON job: %v", err)
|
||||
}
|
||||
|
||||
badRequest := request
|
||||
badRequest.RequestID = "request-rcon-bad"
|
||||
badRequest.JobID = "job-rcon-bad"
|
||||
badRequest.IdempotencyKey = "idem-rcon-bad"
|
||||
if _, err := svc.CreateJob(domain.Job{ID: badRequest.JobID, ServerInstanceID: badRequest.Binding.ServerInstanceID, RunEndpointID: badRequest.Binding.RunEndpointID, Capability: domain.JobCapabilityRemoteRunProtectedRCON, TargetKey: badRequest.TargetKey, InputRef: "input://rcon-template/request-rcon-bad", IdempotencyKey: badRequest.IdempotencyKey, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, ExecutionInput: domain.JobExecutionInput{RemoteAdapterKind: "protected-rcon", SourceRCON: scumTypedRCONSourcePlan(), RCONTemplate: &badRequest}}); err != nil {
|
||||
t.Fatalf("create bad typed RCON job: %v", err)
|
||||
}
|
||||
badClaim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: badRequest.Binding.RunEndpointID, SessionToken: sessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunProtectedRCON}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||
if err != nil {
|
||||
t.Fatalf("claim bad typed RCON job: %v", err)
|
||||
}
|
||||
badResult := scumTypedRCONTemplateTestResult(badRequest)
|
||||
badResult.PayloadDigest = "sha256:" + strings.Repeat("b", 64)
|
||||
_, err = svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: badRequest.Binding.RunEndpointID, SessionToken: sessionToken, JobID: badClaim.Job.JobID, LeaseToken: badClaim.Job.LeaseToken, Attempt: badClaim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "rcon complete"}, Message: "rcon complete", ExecutionResult: domain.JobExecutionResult{Kind: scumRCONTemplateExecutionKind, RCONTemplate: &badResult, AuditSummary: "redacted typed RCON template command"}})
|
||||
if err == nil || !strings.Contains(err.Error(), "digest") {
|
||||
t.Fatalf("expected digest-fenced typed RCON result rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRunJobGuardedMutationEnvelopeIsFencedToLease(t *testing.T) {
|
||||
svc, sessionToken := newRegisteredRunJobService(t)
|
||||
request := scumGuardedMutationTestRequest()
|
||||
createSCUMTemplateServerFixture(t, svc, request.Binding)
|
||||
job, err := svc.CreateJob(domain.Job{ID: request.JobID, ServerInstanceID: request.Binding.ServerInstanceID, RunEndpointID: request.Binding.RunEndpointID, Capability: domain.JobCapabilityRemoteRunProtectedSQL, TargetKey: request.TargetKey, InputRef: "input://guarded-mutation/request-mutation", IdempotencyKey: request.IdempotencyKey, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, ExecutionInput: domain.JobExecutionInput{RemoteAdapterKind: "protected-sql", RemoteAdapterKey: request.TargetKey, GuardedMutation: &request}})
|
||||
if err != nil {
|
||||
t.Fatalf("create guarded mutation job: %v", err)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: request.Binding.RunEndpointID, SessionToken: sessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||
if err != nil || !claim.HasJob || claim.Job.JobID != job.ID || claim.Job.ExecutionInput.GuardedMutation == nil || claim.Job.ExecutionInput.GuardedMutation.Payload["attributeKey"].(string) != "Strength" {
|
||||
t.Fatalf("claim guarded mutation job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
result := scumGuardedMutationTestResult(request)
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: request.Binding.RunEndpointID, SessionToken: sessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "mutation complete"}, Message: "mutation complete", ExecutionResult: domain.JobExecutionResult{Kind: scumGuardedMutationExecutionKind, GuardedMutation: &result, AuditSummary: "redacted guarded mutation"}}); err != nil {
|
||||
t.Fatalf("complete matching guarded mutation job: %v", err)
|
||||
}
|
||||
|
||||
badRequest := request
|
||||
badRequest.RequestID = "request-mutation-bad"
|
||||
badRequest.JobID = "job-mutation-bad"
|
||||
badRequest.IdempotencyKey = "idem-mutation-bad"
|
||||
if _, err := svc.CreateJob(domain.Job{ID: badRequest.JobID, ServerInstanceID: badRequest.Binding.ServerInstanceID, RunEndpointID: badRequest.Binding.RunEndpointID, Capability: domain.JobCapabilityRemoteRunProtectedSQL, TargetKey: badRequest.TargetKey, InputRef: "input://guarded-mutation/request-mutation-bad", IdempotencyKey: badRequest.IdempotencyKey, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, ExecutionInput: domain.JobExecutionInput{RemoteAdapterKind: "protected-sql", RemoteAdapterKey: badRequest.TargetKey, GuardedMutation: &badRequest}}); err != nil {
|
||||
t.Fatalf("create bad guarded mutation job: %v", err)
|
||||
}
|
||||
badClaim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: badRequest.Binding.RunEndpointID, SessionToken: sessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||
if err != nil {
|
||||
t.Fatalf("claim bad guarded mutation job: %v", err)
|
||||
}
|
||||
badResult := scumGuardedMutationTestResult(badRequest)
|
||||
badResult.PatchDigest = "sha256:" + strings.Repeat("b", 64)
|
||||
_, err = svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: badRequest.Binding.RunEndpointID, SessionToken: sessionToken, JobID: badClaim.Job.JobID, LeaseToken: badClaim.Job.LeaseToken, Attempt: badClaim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "mutation complete"}, Message: "mutation complete", ExecutionResult: domain.JobExecutionResult{Kind: scumGuardedMutationExecutionKind, GuardedMutation: &badResult, AuditSummary: "redacted guarded mutation"}})
|
||||
if err == nil || !strings.Contains(err.Error(), "digest") {
|
||||
t.Fatalf("expected digest-fenced guarded mutation result rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRunJobParsedLogBatchEnvelopeIsFencedToLease(t *testing.T) {
|
||||
svc, sessionToken := newRegisteredRunJobService(t)
|
||||
binding := scumTemplateTestBinding()
|
||||
createSCUMTemplateServerFixture(t, svc, binding)
|
||||
source := &domain.RuntimeLogSource{Key: "scum-login-events", Kind: "file.tail", TargetKey: "logs/login", StreamKey: "scum.login", CursorKind: "fingerprint", RetentionDays: 90}
|
||||
job, err := svc.CreateJob(domain.Job{ID: "job-log", ServerInstanceID: binding.ServerInstanceID, RunEndpointID: binding.RunEndpointID, Capability: domain.JobCapabilityLogsBackfill, TargetKey: "logs/scum-login-events", InputRef: "artifact://logs/checkpoint/1", IdempotencyKey: "idem-log", ExecutionInput: domain.JobExecutionInput{PluginID: binding.PluginID, TargetVersion: binding.PluginVersion, LogSource: source, Inputs: map[string]string{"parserKey": "scum-login-log-login-parser", "parserVersion": "scum-login-log-v1", "parserDigest": scumTemplateTestHash(), "adapterVersion": binding.AdapterVersion}}})
|
||||
if err != nil {
|
||||
t.Fatalf("create parsed log backfill job: %v", err)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: binding.RunEndpointID, SessionToken: sessionToken, Capabilities: []string{domain.JobCapabilityLogsBackfill}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||
if err != nil || !claim.HasJob || claim.Job.JobID != job.ID || claim.Job.ExecutionInput.LogSource == nil {
|
||||
t.Fatalf("claim parsed log job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
result := scumParsedLogBatchTestResult(binding, job.ID, *source)
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: binding.RunEndpointID, SessionToken: sessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "log parse complete"}, Message: "log parse complete", ExecutionResult: domain.JobExecutionResult{Kind: scumParsedLogBatchExecutionKind, ParsedLogBatch: &result, AuditSummary: "redacted parsed log batch"}}); err != nil {
|
||||
t.Fatalf("complete matching parsed log job: %v", err)
|
||||
}
|
||||
|
||||
badJob, err := svc.CreateJob(domain.Job{ID: "job-log-bad", ServerInstanceID: binding.ServerInstanceID, RunEndpointID: binding.RunEndpointID, Capability: domain.JobCapabilityLogsBackfill, TargetKey: "logs/scum-login-events", InputRef: "artifact://logs/checkpoint/2", IdempotencyKey: "idem-log-bad", ExecutionInput: domain.JobExecutionInput{PluginID: binding.PluginID, TargetVersion: binding.PluginVersion, LogSource: source, Inputs: map[string]string{"parserKey": "scum-login-log-login-parser", "parserVersion": "scum-login-log-v1", "parserDigest": scumTemplateTestHash(), "adapterVersion": binding.AdapterVersion}}})
|
||||
if err != nil {
|
||||
t.Fatalf("create bad parsed log job: %v", err)
|
||||
}
|
||||
badClaim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: binding.RunEndpointID, SessionToken: sessionToken, Capabilities: []string{domain.JobCapabilityLogsBackfill}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||
if err != nil || !badClaim.HasJob || badClaim.Job.JobID != badJob.ID {
|
||||
t.Fatalf("claim bad parsed log job: claim=%+v err=%v", badClaim, err)
|
||||
}
|
||||
badResult := scumParsedLogBatchTestResult(binding, badJob.ID, *source)
|
||||
badResult.ParserDigest = "sha256:" + strings.Repeat("b", 64)
|
||||
_, err = svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: binding.RunEndpointID, SessionToken: sessionToken, JobID: badClaim.Job.JobID, LeaseToken: badClaim.Job.LeaseToken, Attempt: badClaim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "log parse complete"}, Message: "log parse complete", ExecutionResult: domain.JobExecutionResult{Kind: scumParsedLogBatchExecutionKind, ParsedLogBatch: &badResult, AuditSummary: "redacted parsed log batch"}})
|
||||
if err == nil || !strings.Contains(err.Error(), "parser identity") {
|
||||
t.Fatalf("expected parser-fenced parsed log result rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRunJobReconcile(t *testing.T) {
|
||||
svc, sessionToken := newRegisteredRunJobService(t)
|
||||
createQueuedRunJob(t, svc, "job-1", "idem-1")
|
||||
@@ -384,7 +242,7 @@ func newRegisteredRunJobService(t *testing.T) (*CoreService, string) {
|
||||
t.Helper()
|
||||
svc := newTestCoreService()
|
||||
helloRequest := validRunControlHello()
|
||||
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, "process.start", domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunProtectedRCON, domain.JobCapabilityRemoteRunProtectedSQL, domain.JobCapabilityLogsBackfill)
|
||||
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, "process.start")
|
||||
helloRequest.CapabilityReport.Fingerprint = "cap-jobs"
|
||||
hello, err := svc.RegisterRunHello(helloRequest)
|
||||
if err != nil {
|
||||
@@ -393,51 +251,6 @@ func newRegisteredRunJobService(t *testing.T) (*CoreService, string) {
|
||||
return svc, hello.SessionToken
|
||||
}
|
||||
|
||||
func scumTemplateTestHash() string { return "sha256:" + strings.Repeat("a", 64) }
|
||||
|
||||
func scumTemplateTestBinding() domain.SCUMBindingIdentity {
|
||||
return domain.SCUMBindingIdentity{ServerInstanceID: "server-scum", RunBindingID: "binding-scum", RunEndpointID: "run-local", PluginID: "game.scum", PluginVersion: "0.1.6", AdapterVersion: "adapter-1", GameVersion: "scum-1", DatabaseIdentity: "scum-database"}
|
||||
}
|
||||
|
||||
func scumTypedRCONTemplateTestRequest() domain.SCUMTypedRCONTemplateRequest {
|
||||
return domain.SCUMTypedRCONTemplateRequest{RequestID: "request-rcon", JobID: "job-rcon", Binding: scumTemplateTestBinding(), Capability: domain.SCUMDataCapabilityEconomyCommand, TransportKey: "scum-rcon", TargetKey: "scum-rcon", TemplateKey: "economy.fame.set.v1", AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumTemplateTestHash(), AssetDigest: scumTemplateTestHash(), PayloadDigest: scumTemplateTestHash(), ConfirmationDigest: scumTemplateTestHash(), TargetIdentityDigest: scumTemplateTestHash(), IdempotencyKey: "idem-rcon", Payload: map[string]any{"externalPlayerId": "player-redacted", "absoluteValue": 100.0}, ReviewReason: "operator reviewed absolute fame update", Bounds: domain.DefaultSCUMTypedRCONTemplateBounds(), RequestedAt: time.Now()}
|
||||
}
|
||||
|
||||
func scumGuardedMutationTestRequest() domain.SCUMGuardedMutationRequest {
|
||||
return domain.SCUMGuardedMutationRequest{RequestID: "request-mutation", JobID: "job-mutation", Binding: scumTemplateTestBinding(), Capability: domain.SCUMDataCapabilityProfileXMLWrite, TargetKey: "scum-mutation-db", TemplateKey: "profile.attributes.patch.v1", AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumTemplateTestHash(), AssetDigest: scumTemplateTestHash(), TargetIdentityDigest: scumTemplateTestHash(), ExpectedRowDigest: scumTemplateTestHash(), ExpectedValueDigest: scumTemplateTestHash(), ExpectedXMLDigest: scumTemplateTestHash(), PatchDigest: scumTemplateTestHash(), BackupEvidenceDigest: scumTemplateTestHash(), OfflineEvidenceDigest: scumTemplateTestHash(), DangerConfirmationDigest: scumTemplateTestHash(), ReadbackExpectationDigest: scumTemplateTestHash(), IdempotencyKey: "idem-mutation", Payload: map[string]any{"attributeKey": "Strength", "absoluteValue": 8.5}, ReviewReason: "operator confirmed offline profile attribute patch", Bounds: domain.DefaultSCUMGuardedMutationBounds(), RequestedAt: time.Now()}
|
||||
}
|
||||
|
||||
func scumTypedRCONSourcePlan() *domain.RuntimeSourceRCONPlan {
|
||||
return &domain.RuntimeSourceRCONPlan{Protocol: "source-rcon", ExtensionKey: "scum-rcon", ModKey: "scum_simple_rcon", ConfigRef: "ue4ss/Mods/scum_simple_rcon/config.ini", DeploymentStateRef: "runtime/ue4ss-dll/scum-rcon/release.json", Port: 27015}
|
||||
}
|
||||
|
||||
func createSCUMTemplateServerFixture(t *testing.T, svc *CoreService, binding domain.SCUMBindingIdentity) {
|
||||
t.Helper()
|
||||
if _, err := svc.CreateGamePlugin(domain.GamePlugin{ID: binding.PluginID, Name: "SCUM", Version: binding.PluginVersion, ServerType: "scum", ManifestRef: "artifact://manifests/game.scum/0.1.6", CreateFormSchemaRef: "artifact://schemas/game.scum/create-form/0.1.6", RequiredRunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunProtectedRCON, domain.JobCapabilityRemoteRunProtectedSQL, domain.JobCapabilityLogsBackfill}, DeclaredPermissions: []string{"server.remote.access", "server.game-client.command", "server.game-client.maintenance", "server.logs.read"}, Permissions: domain.PluginPermissions{RemoteAccess: true}}); err != nil {
|
||||
t.Fatalf("create SCUM plugin fixture: %v", err)
|
||||
}
|
||||
if _, err := svc.CreateServerInstance(domain.ServerInstance{ID: binding.ServerInstanceID, PluginID: binding.PluginID, RunEndpointID: binding.RunEndpointID, Name: "SCUM"}); err != nil {
|
||||
t.Fatalf("create SCUM server fixture: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func scumTemplateTestResult(request domain.SCUMSQLiteTemplateRequest) domain.SCUMSQLiteTemplateResult {
|
||||
return domain.SCUMSQLiteTemplateResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, ParameterDigest: request.ParameterDigest, SourceFingerprint: scumTemplateTestHash(), ObservedAt: time.Now(), ResultDigest: scumTemplateTestHash(), RowCount: 1, Rows: []map[string]any{{"externalPlayerId": "player-redacted", "displayName": "Known Player"}}, Limits: request.Bounds, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||
}
|
||||
|
||||
func scumTypedRCONTemplateTestResult(request domain.SCUMTypedRCONTemplateRequest) domain.SCUMTypedRCONTemplateResult {
|
||||
return domain.SCUMTypedRCONTemplateResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TransportKey: request.TransportKey, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, PayloadDigest: request.PayloadDigest, ConfirmationDigest: request.ConfirmationDigest, TargetIdentityDigest: request.TargetIdentityDigest, ObservedAt: time.Now(), ResultDigest: scumTemplateTestHash(), ResponseDigest: scumTemplateTestHash(), ConfirmationStatus: domain.SCUMRCONConfirmationConfirmed, ConfirmationDigestID: scumTemplateTestHash(), SafeSummary: "confirmed by declared readback", Limits: request.Bounds, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||
}
|
||||
|
||||
func scumGuardedMutationTestResult(request domain.SCUMGuardedMutationRequest) domain.SCUMGuardedMutationResult {
|
||||
return domain.SCUMGuardedMutationResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, SourceFingerprint: scumTemplateTestHash(), TargetIdentityDigest: request.TargetIdentityDigest, ExpectedRowDigest: request.ExpectedRowDigest, ExpectedValueDigest: request.ExpectedValueDigest, ExpectedXMLDigest: request.ExpectedXMLDigest, PatchDigest: request.PatchDigest, BackupEvidenceDigest: request.BackupEvidenceDigest, OfflineEvidenceDigest: request.OfflineEvidenceDigest, DangerConfirmationDigest: request.DangerConfirmationDigest, ReadbackExpectationDigest: request.ReadbackExpectationDigest, ObservedAt: time.Now(), ResultDigest: scumTemplateTestHash(), BeforeDigest: scumTemplateTestHash(), AfterDigest: scumTemplateTestHash(), ReadbackDigest: scumTemplateTestHash(), AffectedRows: 1, ReadbackStatus: domain.SCUMMutationReadbackConfirmed, SafeSummary: "confirmed by declared readback", Limits: request.Bounds, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||
}
|
||||
|
||||
func scumParsedLogBatchTestResult(binding domain.SCUMBindingIdentity, jobID string, source domain.RuntimeLogSource) domain.SCUMParsedLogBatchResult {
|
||||
cursor := domain.SCUMParsedLogCursor{SourceIdentityDigest: scumTemplateTestHash(), StreamGeneration: scumTemplateTestHash(), Sequence: 7}
|
||||
return domain.SCUMParsedLogBatchResult{RequestID: "request-log", JobID: jobID, Binding: binding, Status: domain.SCUMTerminalResultSucceeded, SourceKey: source.Key, StreamKey: source.StreamKey, ParserKey: "scum-login-log-login-parser", ParserVersion: "scum-login-log-v1", AdapterVersion: binding.AdapterVersion, AssetDigest: scumTemplateTestHash(), ParserDigest: scumTemplateTestHash(), ObservedAt: time.Now(), ResultDigest: scumTemplateTestHash(), FirstCursor: cursor, LastCursor: cursor, TailState: domain.SCUMLogTailRotated, Replay: true, EventCount: 1, Events: []domain.SCUMParsedLogEvent{{EventType: "scum.login", OccurredAt: time.Now(), Cursor: cursor, LogicalEventDigest: scumTemplateTestHash(), EventDigest: scumTemplateTestHash(), PayloadDigest: scumTemplateTestHash(), Payload: map[string]any{"externalPlayerId": "player-redacted", "displayName": "Known Player", "profileLocalId": "profile-redacted"}}}, SafeSummary: "one sanitized login event parsed from declared source", Limits: domain.DefaultSCUMParsedLogBatchBounds(), SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||
}
|
||||
|
||||
func createQueuedRunJob(t *testing.T, svc *CoreService, id string, idempotencyKey string) domain.Job {
|
||||
t.Helper()
|
||||
job, err := svc.CreateJob(domain.Job{
|
||||
|
||||
@@ -44,9 +44,6 @@ func (svc *CoreService) RequestRemoteAdapterForSession(sessionID string, request
|
||||
if err := validator.ValidateRemoteAdapterRequest(request); err != nil {
|
||||
return domain.RemoteAdapterResult{}, err
|
||||
}
|
||||
if request.Capability == domain.JobCapabilityRemoteRunDBSQLiteProbe && !request.PlatformScheduled {
|
||||
return domain.RemoteAdapterResult{}, forbiddenError("schema probe is scheduled by Platform and is not a direct remote-adapter request")
|
||||
}
|
||||
instance, err := svc.GetServerInstanceForSession(sessionID, request.ServerInstanceID)
|
||||
if err != nil {
|
||||
return domain.RemoteAdapterResult{}, err
|
||||
@@ -86,30 +83,20 @@ func (svc *CoreService) RequestRemoteAdapterForSession(sessionID string, request
|
||||
return domain.RemoteAdapterResult{}, validationError("remote adapter timeout or retry exceeds declaration")
|
||||
}
|
||||
inputRef := request.InputRef
|
||||
isSchemaProbe := request.Capability == domain.JobCapabilityRemoteRunDBSQLiteProbe && request.PlatformScheduled && request.SQLiteSchemaProbe != nil
|
||||
if inputRef == "" && !isSchemaProbe {
|
||||
if inputRef == "" {
|
||||
inputRef = fmt.Sprintf("input://remote-adapters/%s/%s", instance.ID, request.DeclarationKey)
|
||||
}
|
||||
targetKey := request.TargetKey
|
||||
executionInput := domain.JobExecutionInput{WorkspaceScope: svc.runtimeProfileScope(instance.ID), RemoteAdapterKey: selected.Key, RemoteAdapterKind: string(selected.Kind), TimeoutSeconds: timeout, Inputs: domain.CopyStringMap(request.Inputs), SQLiteSchemaProbe: domain.CopySCUMSchemaProbeRequestPtr(request.SQLiteSchemaProbe)}
|
||||
if isSchemaProbe {
|
||||
targetKey = sqliteSchemaProbeRunTargetKey(request.TargetKey)
|
||||
inputRef = ""
|
||||
executionInput.RemoteAdapterKey = ""
|
||||
executionInput.RemoteAdapterKind = ""
|
||||
executionInput.Inputs = nil
|
||||
}
|
||||
job := domain.Job{
|
||||
ID: jobIDFromParts("job-remote-adapter", instance.ID, request.IdempotencyKey),
|
||||
ServerInstanceID: instance.ID,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
Capability: request.Capability,
|
||||
TargetKey: targetKey,
|
||||
TargetKey: request.TargetKey,
|
||||
InputRef: inputRef,
|
||||
IdempotencyKey: request.IdempotencyKey,
|
||||
Progress: domain.JobProgress{Percent: 0, Message: "scoped remote adapter queued"},
|
||||
RetryPolicy: domain.JobRetryPolicy{MaxAttempts: attempts, InitialBackoffSeconds: 2, MaxBackoffSeconds: 30},
|
||||
ExecutionInput: executionInput,
|
||||
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: svc.runtimeProfileScope(instance.ID), RemoteAdapterKey: selected.Key, RemoteAdapterKind: string(selected.Kind), TimeoutSeconds: timeout, Inputs: domain.CopyStringMap(request.Inputs)},
|
||||
}
|
||||
created, err := svc.CreateJob(job)
|
||||
if err != nil {
|
||||
@@ -126,14 +113,6 @@ func (svc *CoreService) RequestRemoteAdapterForSession(sessionID string, request
|
||||
return domain.RemoteAdapterResult{RequestID: created.ID, ServerInstanceID: instance.ID, DeclarationKey: selected.Key, TargetKey: request.TargetKey, Kind: selected.Kind, Status: string(created.State), Retryable: attempts > 1, Message: "scoped remote adapter queued", ResultRef: "job://" + created.ID, AuditEventID: auditID}, nil
|
||||
}
|
||||
|
||||
func sqliteSchemaProbeRunTargetKey(targetKey string) string {
|
||||
trimmed := strings.TrimSpace(targetKey)
|
||||
if strings.HasPrefix(trimmed, "databases/") {
|
||||
return trimmed
|
||||
}
|
||||
return "databases/" + trimmed
|
||||
}
|
||||
|
||||
func intersectRemoteCapabilities(profile []string, declared []string, endpoint []string) []string {
|
||||
result := make([]string, 0, len(profile))
|
||||
for _, capability := range profile {
|
||||
@@ -150,7 +129,7 @@ func isRemoteAdapterCapability(capability string) bool {
|
||||
domain.JobCapabilityRemoteRsyncRead, domain.JobCapabilityRemoteRsyncWrite,
|
||||
domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunFilesWrite,
|
||||
domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop,
|
||||
domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteProbe, domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand:
|
||||
return true
|
||||
default:
|
||||
@@ -187,7 +166,7 @@ func remoteAdapterKindForCapability(capability string) domain.RemoteAdapterKind
|
||||
return domain.RemoteAdapterRunFile
|
||||
case domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop:
|
||||
return domain.RemoteAdapterRunProcess
|
||||
case domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteProbe, domain.JobCapabilityRemoteRunDBSQLiteQuery:
|
||||
case domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteQuery:
|
||||
return domain.RemoteAdapterDatabase
|
||||
case domain.JobCapabilityRemoteRunRCONCommand:
|
||||
return domain.RemoteAdapterRCON
|
||||
|
||||
@@ -2,7 +2,6 @@ package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
@@ -87,125 +86,3 @@ func TestRemoteAdapterRequestPropagatesTypedInputsToRunJob(t *testing.T) {
|
||||
t.Fatal("real Run claim aliases persisted remote inputs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMSchemaProbeDispatchIsPlatformScheduledAndFenced(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||
plugin.Permissions.RemoteAccess = true
|
||||
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.remote.access")
|
||||
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
|
||||
plugin.RemoteAccess = domain.GamePluginRemoteAccess{Methods: []string{"run"}, RunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}, DatabaseEngines: []string{"sqlite"}}
|
||||
plugin.RuntimeProfiles.TransportProfiles = append(plugin.RuntimeProfiles.TransportProfiles,
|
||||
domain.RuntimeTransportProfile{Key: "server-files", Kind: "file", TargetKey: "server-root", Capabilities: []string{domain.JobCapabilityRemoteRunFilesRead}},
|
||||
domain.RuntimeTransportProfile{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}},
|
||||
)
|
||||
plugin.RuntimeProfiles.DataTargets = append(plugin.RuntimeProfiles.DataTargets, domain.RuntimeDataTarget{Key: "scum-database", Kind: "sqlite.snapshot", TransportKey: "scum-database", SourceRootKey: "server-root", SourcePath: "SCUM/Saved/SaveFiles/SCUM.db", WorkspaceKey: "databases/scum-database", RefreshPolicy: "on-demand-snapshot", MaxBytes: 1024 * 1024 * 1024, Platforms: []string{"windows"}})
|
||||
plugin.RuntimeProfiles.LifecycleProfiles[0].TransportKeys = append(plugin.RuntimeProfiles.LifecycleProfiles[0].TransportKeys, "scum-database")
|
||||
plugin.SCUMLiveData = domain.SCUMLiveDataManifest{SchemaVersion: "1", Probe: domain.SCUMSchemaProbeDeclaration{Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, TargetKey: "scum-database", Bounds: domain.DefaultSCUMSchemaProbeBounds()}, CapabilityGates: []domain.SCUMLiveDataCapabilityGateDeclaration{{Capability: domain.SCUMDataCapabilitySchemaProbe, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "scum-live-data-v0", EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "waiting for current service evidence"}}}
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
|
||||
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
session := createServiceUserAndLogin(t, svc, domain.User{ID: "user-scum-probe-owner", DisplayName: "SCUM Probe Owner", Email: "scum-probe-owner@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
|
||||
instance, err := svc.CreateServerInstanceForSession(session, domain.ServerInstance{ID: "server-scum-probe", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "SCUM Probe"})
|
||||
if err != nil {
|
||||
t.Fatalf("create server instance: %v", err)
|
||||
}
|
||||
createCompleteRuntimeBinding(t, svc, instance, "local")
|
||||
|
||||
_, err = svc.RequestRemoteAdapterForSession(session, domain.RemoteAdapterRequest{ServerInstanceID: instance.ID, DeclarationKey: "scum-database", TargetKey: "scum-database", Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, IdempotencyKey: "direct-probe-denied", InputRef: "input://scum-schema-probe/direct-probe-denied"})
|
||||
if err == nil || !strings.Contains(err.Error(), "scheduled by Platform") {
|
||||
t.Fatalf("expected public probe request denial, got %v", err)
|
||||
}
|
||||
endpoint.Capabilities = withoutCapability(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
|
||||
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err = svc.RequestSCUMSchemaProbeForSession(session, instance.ID, "probe-missing-run-capability"); err == nil || !strings.Contains(err.Error(), "does not expose") {
|
||||
t.Fatalf("expected missing active Run capability, got %v", err)
|
||||
}
|
||||
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
|
||||
if err != nil || len(jobs) != 0 {
|
||||
t.Fatalf("missing probe executor must not create jobs: len=%d err=%v", len(jobs), err)
|
||||
}
|
||||
|
||||
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
|
||||
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
probeRequest, queued, err := svc.RequestSCUMSchemaProbeForSession(session, instance.ID, "probe-current-schema")
|
||||
if err != nil {
|
||||
t.Fatalf("queue SCUM schema probe: %v", err)
|
||||
}
|
||||
job, err := svc.store.Jobs().Get(queued.RequestID)
|
||||
if err != nil {
|
||||
t.Fatalf("get probe job: %v", err)
|
||||
}
|
||||
if job.Capability != domain.JobCapabilityRemoteRunDBSQLiteProbe || job.TargetKey != "databases/scum-database" || job.InputRef != "" || job.ExecutionInput.RemoteAdapterKey != "" || job.ExecutionInput.RemoteAdapterKind != "" || len(job.ExecutionInput.Inputs) != 0 {
|
||||
t.Fatalf("unexpected probe job envelope: %+v", job)
|
||||
}
|
||||
if job.ExecutionInput.SQLiteSchemaProbe == nil || job.ExecutionInput.SQLiteSchemaProbe.RequestID != probeRequest.RequestID || job.ExecutionInput.SQLiteSchemaProbe.Binding.DatabaseIdentity != "scum-database" || job.ExecutionInput.SQLiteSchemaProbe.Bounds.MaxResultBytes != 524288 {
|
||||
t.Fatalf("probe job did not include typed SQLite schema probe request: %+v", job.ExecutionInput.SQLiteSchemaProbe)
|
||||
}
|
||||
|
||||
helloRequest := validRunControlHello()
|
||||
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
|
||||
helloRequest.CapabilityReport.Fingerprint = "cap-scum-probe"
|
||||
hello, err := svc.RegisterRunHello(helloRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("register Run hello: %v", err)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: endpoint.ID, SessionToken: hello.SessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}, Capacity: domain.RunCapacity{MaxJobs: 1}})
|
||||
if err != nil {
|
||||
t.Fatalf("claim probe job: %v", err)
|
||||
}
|
||||
if !claim.HasJob || claim.Job == nil || claim.Job.TargetKey != "databases/scum-database" || claim.Job.InputRef != "" || claim.Job.FencingToken == 0 || claim.Job.MaxAttempts != 1 || claim.Job.ExecutionInput.RemoteAdapterKey != "" || len(claim.Job.ExecutionInput.Inputs) != 0 {
|
||||
t.Fatalf("claimed probe job lost fenced typed envelope: %+v", claim.Job)
|
||||
}
|
||||
if claim.Job.ExecutionInput.SQLiteSchemaProbe == nil || claim.Job.ExecutionInput.SQLiteSchemaProbe.RequestID != probeRequest.RequestID || claim.Job.ExecutionInput.SQLiteSchemaProbe.Binding.RunBindingID != probeRequest.Binding.RunBindingID {
|
||||
t.Fatalf("claimed probe job lost typed schema probe request: %+v", claim.Job.ExecutionInput.SQLiteSchemaProbe)
|
||||
}
|
||||
assignmentBody := dto.RunJobAssignmentFromDomain(*claim.Job)
|
||||
payload, err := json.Marshal(assignmentBody)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal probe Run assignment: %v", err)
|
||||
}
|
||||
var runWire struct {
|
||||
ExecutionInput struct {
|
||||
SQLiteSchemaProbe struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Bounds *dto.SCUMSchemaProbeBoundsDTO `json:"bounds"`
|
||||
Limits dto.SCUMSchemaProbeBoundsDTO `json:"limits"`
|
||||
} `json:"sqliteSchemaProbe"`
|
||||
} `json:"executionInput"`
|
||||
}
|
||||
if err := json.Unmarshal(payload, &runWire); err != nil {
|
||||
t.Fatalf("unmarshal probe Run assignment: %v", err)
|
||||
}
|
||||
if runWire.ExecutionInput.SQLiteSchemaProbe.RequestID != probeRequest.RequestID || runWire.ExecutionInput.SQLiteSchemaProbe.JobID != "" || runWire.ExecutionInput.SQLiteSchemaProbe.Bounds != nil || runWire.ExecutionInput.SQLiteSchemaProbe.Limits.MaxResultBytes != probeRequest.Bounds.MaxResultBytes {
|
||||
t.Fatalf("probe Run assignment JSON does not match Run contract: %s", payload)
|
||||
}
|
||||
badBinding := probeRequest.Binding
|
||||
badBinding.RunBindingID = "runtime-binding-other"
|
||||
badProbe := domain.SCUMSchemaProbeResult{RequestID: probeRequest.RequestID, JobID: probeRequest.JobID, Binding: badBinding, Status: domain.SCUMCapabilityEvidenceCompatible, SourceFingerprint: "sha256:" + strings.Repeat("c", 64), SchemaFingerprint: "sha256:" + strings.Repeat("a", 64), ObservedAt: fixedTime, ResultDigest: "sha256:" + strings.Repeat("b", 64), Limits: probeRequest.Bounds}
|
||||
_, err = svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: endpoint.ID, SessionToken: hello.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "probe complete"}, Message: "probe complete", ExecutionResult: domain.JobExecutionResult{Kind: scumSchemaProbeExecutionKind, SQLiteSchemaProbe: &badProbe, AuditSummary: "redacted schema probe"}})
|
||||
if err == nil || !strings.Contains(err.Error(), "binding identity") {
|
||||
t.Fatalf("expected binding mismatch rejection, got %v", err)
|
||||
}
|
||||
goodProbe := badProbe
|
||||
goodProbe.Binding = probeRequest.Binding
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: endpoint.ID, SessionToken: hello.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "probe complete"}, Message: "probe complete", ExecutionResult: domain.JobExecutionResult{Kind: scumSchemaProbeExecutionKind, SQLiteSchemaProbe: &goodProbe, AuditSummary: "redacted schema probe"}}); err != nil {
|
||||
t.Fatalf("complete fenced probe job: %v", err)
|
||||
}
|
||||
stored, err := svc.store.Jobs().Get(job.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get completed probe job: %v", err)
|
||||
}
|
||||
if stored.ExecutionResult.SQLiteSchemaProbe == nil || stored.ExecutionResult.SQLiteSchemaProbe.SourceFingerprint != goodProbe.SourceFingerprint || stored.ExecutionResult.SQLiteSchemaProbe.ResultDigest != goodProbe.ResultDigest || stored.ExecutionResult.SQLiteSchemaProbe.Binding.RunBindingID != probeRequest.Binding.RunBindingID {
|
||||
t.Fatalf("typed probe result was not persisted safely: %+v", stored.ExecutionResult.SQLiteSchemaProbe)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,8 +124,6 @@ type Core interface {
|
||||
ListBackupsForSession(string, domain.BackupFilter) ([]domain.BackupRecord, error)
|
||||
ListRemoteAdapterDeclarationsForSession(string, string) ([]domain.RemoteAdapterDeclaration, error)
|
||||
RequestRemoteAdapterForSession(string, domain.RemoteAdapterRequest) (domain.RemoteAdapterResult, error)
|
||||
NegotiateSCUMCapabilitiesForSession(string, string) (domain.SCUMCapabilityNegotiation, error)
|
||||
RequestSCUMSchemaProbeForSession(string, string, string) (domain.SCUMSchemaProbeRequest, domain.RemoteAdapterResult, error)
|
||||
GetServerConfigForSession(string, string) (domain.ServerConfig, error)
|
||||
GetDeclaredFileReadSnapshotForSession(string, string, string) (domain.DeclaredFileReadSnapshot, error)
|
||||
PreviewServerConfigWriteForSession(string, domain.ServerConfigDiffRequest) (domain.ServerConfigDiffPreview, error)
|
||||
@@ -227,6 +225,20 @@ type Core interface {
|
||||
RequestGameGiftGrantForSession(string, string, domain.GameGiftGrantRequest) (domain.GameGiftGrant, error)
|
||||
ApproveGameGiftGrantForSession(string, string) (domain.GameGiftGrant, error)
|
||||
ListGameGiftGrantsForSession(string, string) ([]domain.GameGiftGrant, error)
|
||||
ListSCUMPlayerLiveStatesForSession(string, domain.SCUMProjectionFilter) ([]domain.SCUMPlayerLiveState, error)
|
||||
ListSCUMSquadsForSession(string, domain.SCUMProjectionFilter) ([]domain.SCUMSquad, error)
|
||||
ListSCUMSquadMembersForSession(string, domain.SCUMProjectionFilter) ([]domain.SCUMSquadMember, error)
|
||||
ListSCUMVehiclesForSession(string, domain.SCUMProjectionFilter) ([]domain.SCUMVehicle, error)
|
||||
ListSCUMFlagsForSession(string, domain.SCUMProjectionFilter) ([]domain.SCUMFlag, error)
|
||||
ListSCUMCurrentPositionsForSession(string, domain.SCUMProjectionFilter) ([]domain.SCUMCurrentPosition, error)
|
||||
RequestSCUMOperationForSession(string, string, domain.SCUMOperationRequest) (domain.SCUMOperationRequest, error)
|
||||
ListSCUMOperationsForSession(string, domain.SCUMOperationRequestFilter) ([]domain.SCUMOperationRequest, error)
|
||||
ApproveSCUMOperationForSession(string, string) (domain.SCUMOperationRequest, error)
|
||||
ReconcileSCUMOperation(string) (domain.SCUMOperationRequest, error)
|
||||
ConfirmSCUMOperation(string, domain.SCUMOperationConfirmation) (domain.SCUMOperationRequest, error)
|
||||
CreateSCUMWorkflowForSession(string, string, domain.SCUMWorkflowInstance) (domain.SCUMWorkflowInstance, error)
|
||||
ListSCUMWorkflowsForSession(string, domain.SCUMWorkflowInstanceFilter) ([]domain.SCUMWorkflowInstance, error)
|
||||
ListSCUMWorkflowStepsForSession(string, domain.SCUMWorkflowStepFilter) ([]domain.SCUMWorkflowStep, error)
|
||||
CreateAuditEvent(domain.AuditEvent) (domain.AuditEvent, error)
|
||||
GetAuditEvent(string) (domain.AuditEvent, error)
|
||||
ListAuditEvents(domain.AuditEventFilter) ([]domain.AuditEvent, error)
|
||||
@@ -814,7 +826,6 @@ func gamePluginFromManifestRegistration(registration domain.GamePluginManifestRe
|
||||
RemoteAccess: manifest.RemoteAccess,
|
||||
RuntimeProfiles: manifest.RuntimeProfiles,
|
||||
GameClientBridge: manifest.GameClientBridge,
|
||||
SCUMLiveData: manifest.SCUMLiveData,
|
||||
MapTrajectories: manifest.MapTrajectories,
|
||||
Status: domain.GamePluginStatusInstalled,
|
||||
}
|
||||
@@ -1169,11 +1180,6 @@ func (svc *CoreService) executeBridgeRemoteAccessRequest(sessionID string, base
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: "capability_denied", Message: "requested remote capability is not declared by plugin"}
|
||||
return base
|
||||
}
|
||||
if capability == domain.JobCapabilityRemoteRunDBSQLiteProbe {
|
||||
base.Status = "denied"
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: "capability_denied", Message: "schema probe is scheduled by Platform and is not a plugin page action"}
|
||||
return base
|
||||
}
|
||||
declarationKey := strings.TrimSpace(payload["declarationKey"])
|
||||
if declarationKey == "" {
|
||||
for _, profile := range plugin.RuntimeProfiles.TransportProfiles {
|
||||
|
||||
@@ -1,397 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/repo"
|
||||
"browser.local/platform/validator"
|
||||
)
|
||||
|
||||
const (
|
||||
scumSchemaProbeExecutionKind = "sqlite.schema-probe"
|
||||
scumSQLiteTemplateExecutionKind = "sqlite.template-query"
|
||||
scumRCONTemplateExecutionKind = "rcon.template-command"
|
||||
scumGuardedMutationExecutionKind = "sqlite.guarded-mutation"
|
||||
scumParsedLogBatchExecutionKind = "log.parsed-events"
|
||||
)
|
||||
|
||||
func (svc *CoreService) NegotiateSCUMCapabilitiesForSession(sessionID, serverInstanceID string) (domain.SCUMCapabilityNegotiation, error) {
|
||||
instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID)
|
||||
if err != nil {
|
||||
return domain.SCUMCapabilityNegotiation{}, err
|
||||
}
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
return domain.SCUMCapabilityNegotiation{}, err
|
||||
}
|
||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
||||
if err != nil {
|
||||
return domain.SCUMCapabilityNegotiation{}, err
|
||||
}
|
||||
adapterVersion := scumSchemaProbeAdapterVersion(plugin.SCUMLiveData)
|
||||
active := domain.SCUMBindingIdentity{ServerInstanceID: instance.ID, RunEndpointID: instance.RunEndpointID, PluginID: plugin.ID, PluginVersion: plugin.Version, AdapterVersion: adapterVersion, DatabaseIdentity: scumSchemaProbeDatabaseIdentity(plugin.SCUMLiveData.Probe.TargetKey)}
|
||||
binding, bindingErr := svc.runtimeBindingForServer(instance.ID)
|
||||
if bindingErr == nil {
|
||||
binding, bindingErr = normalizeRuntimeBinding(plugin, binding)
|
||||
}
|
||||
if bindingErr == nil {
|
||||
active.RunBindingID = binding.ID
|
||||
}
|
||||
probeExecutorAvailable := scumRunCapabilityAvailable(endpoint, domain.SCUMDataCapabilitySchemaProbe)
|
||||
negotiation := domain.SCUMCapabilityNegotiation{ServerInstanceID: instance.ID, RunEndpointID: instance.RunEndpointID, RunBindingID: active.RunBindingID, PluginID: plugin.ID, PluginVersion: plugin.Version, AdapterVersion: adapterVersion, GameVersion: active.GameVersion, DatabaseIdentity: active.DatabaseIdentity, ProbeExecutorAvailable: probeExecutorAvailable, EvaluatedAt: svc.now()}
|
||||
evidenceByCapability := svc.latestSCUMCapabilityEvidenceByCapability(instance.ID)
|
||||
for _, declaration := range plugin.SCUMLiveData.CapabilityGates {
|
||||
gate := domain.SCUMCapabilityGate{Capability: declaration.Capability, State: domain.SCUMCapabilityGateDisabled, ReasonCode: domain.SCUMSafeErrorProbeMissing, Reason: safeSCUMGateReason(declaration.SafeReason, "current-service evidence is required before this SCUM capability can run")}
|
||||
if requiredRunCapability := scumRequiredRunCapabilityForDataCapability(declaration.Capability); requiredRunCapability != "" && !containsString(endpoint.Capabilities, requiredRunCapability) {
|
||||
gate.ReasonCode = domain.SCUMSafeErrorProbeExecutorAbsent
|
||||
gate.Reason = "bound Run does not expose the generic executor required for this SCUM capability"
|
||||
negotiation.Gates = append(negotiation.Gates, gate)
|
||||
continue
|
||||
}
|
||||
if bindingErr != nil || binding.Status != domain.RuntimeBindingStatusComplete || binding.PluginVersion != plugin.Version {
|
||||
gate.ReasonCode = domain.SCUMSafeErrorBindingMismatch
|
||||
gate.Reason = "active runtime binding is missing, incomplete, or stale for this plugin version"
|
||||
negotiation.Gates = append(negotiation.Gates, gate)
|
||||
continue
|
||||
}
|
||||
if declaration.Gate != domain.SCUMCapabilityGateEnabled {
|
||||
gate.ReasonCode = scumReasonCodeForEvidenceStatus(declaration.EvidenceStatus)
|
||||
negotiation.Gates = append(negotiation.Gates, gate)
|
||||
continue
|
||||
}
|
||||
requirement := domain.SCUMCapabilityRequirement{Capability: declaration.Capability, AdapterVersion: declaration.AdapterVersion, SchemaFingerprint: declaration.RequiredSchemaFingerprint, AssetDigests: domain.CopyStringSlice(declaration.RequiredAssetDigests)}
|
||||
gate = domain.EvaluateSCUMCapabilityGate(requirement, evidenceByCapability[declaration.Capability], active, probeExecutorAvailable, negotiation.EvaluatedAt)
|
||||
negotiation.Gates = append(negotiation.Gates, gate)
|
||||
}
|
||||
return negotiation, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) latestSCUMCapabilityEvidenceByCapability(serverInstanceID string) map[domain.SCUMDataCapability]domain.SCUMCapabilityEvidence {
|
||||
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: serverInstanceID})
|
||||
if err != nil {
|
||||
return map[domain.SCUMDataCapability]domain.SCUMCapabilityEvidence{}
|
||||
}
|
||||
evidence := map[domain.SCUMDataCapability]domain.SCUMCapabilityEvidence{}
|
||||
for _, job := range jobs {
|
||||
for _, candidate := range scumCapabilityEvidenceFromJob(job) {
|
||||
current, exists := evidence[candidate.Capability]
|
||||
if !exists || current.ObservedAt.Before(candidate.ObservedAt) {
|
||||
evidence[candidate.Capability] = candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
return evidence
|
||||
}
|
||||
|
||||
func scumCapabilityEvidenceFromJob(job domain.Job) []domain.SCUMCapabilityEvidence {
|
||||
var evidence []domain.SCUMCapabilityEvidence
|
||||
if result := job.ExecutionResult.SQLiteSchemaProbe; result != nil {
|
||||
status := domain.SCUMCapabilityEvidenceFailed
|
||||
if result.Status == domain.SCUMSchemaProbeStatusSucceeded || result.Status == domain.SCUMCapabilityEvidenceCompatible {
|
||||
status = domain.SCUMCapabilityEvidenceCompatible
|
||||
} else if result.Status == domain.SCUMCapabilityEvidenceIncompatible {
|
||||
status = domain.SCUMCapabilityEvidenceIncompatible
|
||||
}
|
||||
evidence = append(evidence, domain.SCUMCapabilityEvidence{Capability: domain.SCUMDataCapabilitySchemaProbe, Status: status, Binding: result.Binding, AdapterVersion: result.Binding.AdapterVersion, SchemaFingerprint: result.SchemaFingerprint, ProbeResultDigest: result.ResultDigest, ObservedAt: result.ObservedAt, SafeError: result.SafeError})
|
||||
}
|
||||
if result := job.ExecutionResult.SQLiteTemplate; result != nil {
|
||||
evidence = append(evidence, domain.SCUMCapabilityEvidence{Capability: result.Capability, Status: scumTerminalEvidenceStatus(result.Status), Binding: result.Binding, AdapterVersion: result.AdapterVersion, SchemaFingerprint: result.SchemaFingerprint, ProbeResultDigest: result.ResultDigest, AssetDigests: []string{result.AssetDigest}, ObservedAt: result.ObservedAt, SafeError: result.SafeError})
|
||||
}
|
||||
if result := job.ExecutionResult.RCONTemplate; result != nil {
|
||||
evidence = append(evidence, domain.SCUMCapabilityEvidence{Capability: result.Capability, Status: scumTerminalEvidenceStatus(result.Status), Binding: result.Binding, AdapterVersion: result.AdapterVersion, SchemaFingerprint: result.SchemaFingerprint, ProbeResultDigest: result.ResultDigest, AssetDigests: []string{result.AssetDigest}, ObservedAt: result.ObservedAt, SafeError: result.SafeError})
|
||||
}
|
||||
if result := job.ExecutionResult.GuardedMutation; result != nil {
|
||||
evidence = append(evidence, domain.SCUMCapabilityEvidence{Capability: result.Capability, Status: scumTerminalEvidenceStatus(result.Status), Binding: result.Binding, AdapterVersion: result.AdapterVersion, SchemaFingerprint: result.SchemaFingerprint, ProbeResultDigest: result.ResultDigest, AssetDigests: []string{result.AssetDigest}, ObservedAt: result.ObservedAt, SafeError: result.SafeError})
|
||||
}
|
||||
return evidence
|
||||
}
|
||||
|
||||
func scumTerminalEvidenceStatus(status domain.SCUMTerminalResultStatus) domain.SCUMCapabilityEvidenceStatus {
|
||||
if status == domain.SCUMTerminalResultSucceeded {
|
||||
return domain.SCUMCapabilityEvidenceCompatible
|
||||
}
|
||||
return domain.SCUMCapabilityEvidenceFailed
|
||||
}
|
||||
|
||||
func scumRunCapabilityAvailable(endpoint domain.RunEndpoint, capability domain.SCUMDataCapability) bool {
|
||||
return containsString(endpoint.Capabilities, scumRequiredRunCapabilityForDataCapability(capability))
|
||||
}
|
||||
|
||||
func scumRequiredRunCapabilityForDataCapability(capability domain.SCUMDataCapability) string {
|
||||
switch capability {
|
||||
case domain.SCUMDataCapabilitySchemaProbe:
|
||||
return domain.JobCapabilityRemoteRunDBSQLiteProbe
|
||||
case domain.SCUMDataCapabilityPlayerRead, domain.SCUMDataCapabilityPlayerDetailRead, domain.SCUMDataCapabilitySquadRead, domain.SCUMDataCapabilitySquadMemberRead, domain.SCUMDataCapabilityVehicleRead, domain.SCUMDataCapabilityFlagRead, domain.SCUMDataCapabilityPositionRead:
|
||||
return domain.JobCapabilityRemoteRunDBSQLiteQuery
|
||||
case domain.SCUMDataCapabilityEconomyCommand, domain.SCUMDataCapabilityGiftCommand:
|
||||
return domain.JobCapabilityRemoteRunProtectedRCON
|
||||
case domain.SCUMDataCapabilityProfileXMLWrite:
|
||||
return domain.JobCapabilityRemoteRunProtectedSQL
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func scumReasonCodeForEvidenceStatus(status domain.SCUMCapabilityEvidenceStatus) domain.SCUMSafeErrorCode {
|
||||
switch status {
|
||||
case domain.SCUMCapabilityEvidenceFailed:
|
||||
return domain.SCUMSafeErrorProbeFailed
|
||||
case domain.SCUMCapabilityEvidenceIncompatible:
|
||||
return domain.SCUMSafeErrorSchemaIncompatible
|
||||
default:
|
||||
return domain.SCUMSafeErrorProbeMissing
|
||||
}
|
||||
}
|
||||
|
||||
func safeSCUMGateReason(value, fallback string) string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (svc *CoreService) RequestSCUMSchemaProbeForSession(sessionID, serverInstanceID, idempotencyKey string) (domain.SCUMSchemaProbeRequest, domain.RemoteAdapterResult, error) {
|
||||
idempotencyKey = strings.TrimSpace(idempotencyKey)
|
||||
if idempotencyKey == "" {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("idempotencyKey is required")
|
||||
}
|
||||
instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID)
|
||||
if err != nil {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
||||
}
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
||||
}
|
||||
probe := plugin.SCUMLiveData.Probe
|
||||
if plugin.SCUMLiveData.SchemaVersion == "" || probe.Capability != domain.JobCapabilityRemoteRunDBSQLiteProbe || strings.TrimSpace(probe.TargetKey) == "" {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema probe is not declared by the plugin")
|
||||
}
|
||||
if !scumSchemaProbeHasDataTarget(plugin.RuntimeProfiles, probe.TargetKey) {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema probe target is not declared as a generated Run data target")
|
||||
}
|
||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
||||
if err != nil {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
||||
}
|
||||
if endpoint.Status != domain.RunEndpointStatusOnline && endpoint.Status != domain.RunEndpointStatusDegraded {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("bound Run is not online for SCUM schema probe")
|
||||
}
|
||||
if !containsString(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("bound Run does not expose the generic SQLite schema-probe executor")
|
||||
}
|
||||
binding, err := svc.runtimeBindingForServer(instance.ID)
|
||||
if errors.Is(err, repo.ErrNotFound) {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("runtime binding is required before SCUM schema probe")
|
||||
}
|
||||
if err != nil {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
||||
}
|
||||
adapterVersion := scumSchemaProbeAdapterVersion(plugin.SCUMLiveData)
|
||||
if adapterVersion == "" {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema-probe adapter version is not declared")
|
||||
}
|
||||
bounds := probe.Bounds
|
||||
if bounds.MaxObjects == 0 {
|
||||
bounds = domain.DefaultSCUMSchemaProbeBounds()
|
||||
}
|
||||
jobID := jobIDFromParts("job-remote-adapter", instance.ID, idempotencyKey)
|
||||
request := domain.SCUMSchemaProbeRequest{
|
||||
RequestID: jobID,
|
||||
JobID: jobID,
|
||||
Binding: domain.SCUMBindingIdentity{
|
||||
ServerInstanceID: instance.ID,
|
||||
RunBindingID: binding.ID,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
PluginID: plugin.ID,
|
||||
PluginVersion: plugin.Version,
|
||||
AdapterVersion: adapterVersion,
|
||||
DatabaseIdentity: scumSchemaProbeDatabaseIdentity(probe.TargetKey),
|
||||
},
|
||||
Bounds: bounds,
|
||||
RequestedAt: svc.now(),
|
||||
}
|
||||
if err := validator.ValidateSCUMSchemaProbeRequest(request); err != nil {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
||||
}
|
||||
result, err := svc.RequestRemoteAdapterForSession(sessionID, domain.RemoteAdapterRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
DeclarationKey: probe.TargetKey,
|
||||
TargetKey: probe.TargetKey,
|
||||
Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe,
|
||||
TimeoutSeconds: scumSchemaProbeTimeoutSeconds(bounds),
|
||||
MaxAttempts: 1,
|
||||
IdempotencyKey: idempotencyKey,
|
||||
PlatformScheduled: true,
|
||||
SQLiteSchemaProbe: &request,
|
||||
})
|
||||
if err != nil {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
||||
}
|
||||
return request, result, nil
|
||||
}
|
||||
|
||||
func scumSchemaProbeDatabaseIdentity(targetKey string) string {
|
||||
trimmed := strings.TrimSpace(targetKey)
|
||||
return strings.TrimPrefix(trimmed, "databases/")
|
||||
}
|
||||
|
||||
func scumSchemaProbeHasDataTarget(profiles domain.GamePluginRuntimeProfiles, targetKey string) bool {
|
||||
expectedWorkspaceKey := "databases/" + scumSchemaProbeDatabaseIdentity(targetKey)
|
||||
for _, target := range profiles.DataTargets {
|
||||
if target.Key == targetKey && target.Kind == "sqlite.snapshot" && target.WorkspaceKey == expectedWorkspaceKey && target.RefreshPolicy == "on-demand-snapshot" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func scumSchemaProbeAdapterVersion(manifest domain.SCUMLiveDataManifest) string {
|
||||
for _, gate := range manifest.CapabilityGates {
|
||||
if gate.Capability == domain.SCUMDataCapabilitySchemaProbe {
|
||||
return strings.TrimSpace(gate.AdapterVersion)
|
||||
}
|
||||
}
|
||||
for _, gate := range manifest.CapabilityGates {
|
||||
if strings.TrimSpace(gate.AdapterVersion) != "" {
|
||||
return strings.TrimSpace(gate.AdapterVersion)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func scumSchemaProbeTimeoutSeconds(bounds domain.SCUMSchemaProbeBounds) int {
|
||||
if bounds.TimeoutMS <= 0 {
|
||||
return 1
|
||||
}
|
||||
seconds := (bounds.TimeoutMS + 999) / 1000
|
||||
if seconds <= 0 {
|
||||
return 1
|
||||
}
|
||||
return seconds
|
||||
}
|
||||
|
||||
func validateSCUMSchemaProbeResultForJob(job domain.Job, result domain.SCUMSchemaProbeResult) error {
|
||||
if err := validator.ValidateSCUMSchemaProbeResult(result); err != nil {
|
||||
return err
|
||||
}
|
||||
expected := job.ExecutionInput.SQLiteSchemaProbe
|
||||
if expected == nil {
|
||||
return validationError("SQLite schema probe request is missing from leased job")
|
||||
}
|
||||
if result.JobID != job.ID || result.JobID != expected.JobID || result.RequestID != expected.RequestID {
|
||||
return validationError("SQLite schema probe result does not match leased job identity")
|
||||
}
|
||||
if !sameSCUMSchemaProbeBinding(result.Binding, expected.Binding) {
|
||||
return validationError("SQLite schema probe result does not match leased binding identity")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSCUMSQLiteTemplateResultForJob(job domain.Job, result domain.SCUMSQLiteTemplateResult) error {
|
||||
if err := validator.ValidateSCUMSQLiteTemplateResult(result); err != nil {
|
||||
return err
|
||||
}
|
||||
expected := job.ExecutionInput.SQLiteTemplate
|
||||
if expected == nil {
|
||||
return validationError("SQLite template request is missing from leased job")
|
||||
}
|
||||
if result.JobID != job.ID || result.JobID != expected.JobID || result.RequestID != expected.RequestID {
|
||||
return validationError("SQLite template result does not match leased job identity")
|
||||
}
|
||||
if !sameSCUMSchemaProbeBinding(result.Binding, expected.Binding) {
|
||||
return validationError("SQLite template result does not match leased binding identity")
|
||||
}
|
||||
if result.Capability != expected.Capability || result.TargetKey != expected.TargetKey || result.TemplateKey != expected.TemplateKey || result.AdapterVersion != expected.AdapterVersion || result.SchemaFingerprint != expected.RequiredSchemaFingerprint || result.AssetDigest != expected.AssetDigest || result.ParameterDigest != expected.ParameterDigest {
|
||||
return validationError("SQLite template result does not match leased template, adapter, digest, or parameter identity")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSCUMTypedRCONTemplateResultForJob(job domain.Job, result domain.SCUMTypedRCONTemplateResult) error {
|
||||
if err := validator.ValidateSCUMTypedRCONTemplateResult(result); err != nil {
|
||||
return err
|
||||
}
|
||||
expected := job.ExecutionInput.RCONTemplate
|
||||
if expected == nil {
|
||||
return validationError("typed RCON template request is missing from leased job")
|
||||
}
|
||||
if result.JobID != job.ID || result.JobID != expected.JobID || result.RequestID != expected.RequestID {
|
||||
return validationError("typed RCON template result does not match leased job identity")
|
||||
}
|
||||
if !sameSCUMSchemaProbeBinding(result.Binding, expected.Binding) {
|
||||
return validationError("typed RCON template result does not match leased binding identity")
|
||||
}
|
||||
if result.Capability != expected.Capability || result.TransportKey != expected.TransportKey || result.TargetKey != expected.TargetKey || result.TemplateKey != expected.TemplateKey || result.AdapterVersion != expected.AdapterVersion || result.AssetDigest != expected.AssetDigest || result.PayloadDigest != expected.PayloadDigest || result.ConfirmationDigest != expected.ConfirmationDigest || result.TargetIdentityDigest != expected.TargetIdentityDigest {
|
||||
return validationError("typed RCON template result does not match leased template, target, digest, or payload identity")
|
||||
}
|
||||
if expected.RequiredSchemaFingerprint != "" && result.SchemaFingerprint != expected.RequiredSchemaFingerprint {
|
||||
return validationError("typed RCON template result does not match leased schema fingerprint")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSCUMGuardedMutationResultForJob(job domain.Job, result domain.SCUMGuardedMutationResult) error {
|
||||
if err := validator.ValidateSCUMGuardedMutationResult(result); err != nil {
|
||||
return err
|
||||
}
|
||||
expected := job.ExecutionInput.GuardedMutation
|
||||
if expected == nil {
|
||||
return validationError("guarded mutation request is missing from leased job")
|
||||
}
|
||||
if result.JobID != job.ID || result.JobID != expected.JobID || result.RequestID != expected.RequestID {
|
||||
return validationError("guarded mutation result does not match leased job identity")
|
||||
}
|
||||
if !sameSCUMSchemaProbeBinding(result.Binding, expected.Binding) {
|
||||
return validationError("guarded mutation result does not match leased binding identity")
|
||||
}
|
||||
if result.Capability != expected.Capability || result.TargetKey != expected.TargetKey || result.TemplateKey != expected.TemplateKey || result.AdapterVersion != expected.AdapterVersion || result.SchemaFingerprint != expected.RequiredSchemaFingerprint || result.AssetDigest != expected.AssetDigest || result.TargetIdentityDigest != expected.TargetIdentityDigest || result.ExpectedRowDigest != expected.ExpectedRowDigest || result.ExpectedValueDigest != expected.ExpectedValueDigest || result.ExpectedXMLDigest != expected.ExpectedXMLDigest || result.PatchDigest != expected.PatchDigest || result.BackupEvidenceDigest != expected.BackupEvidenceDigest || result.OfflineEvidenceDigest != expected.OfflineEvidenceDigest || result.DangerConfirmationDigest != expected.DangerConfirmationDigest || result.ReadbackExpectationDigest != expected.ReadbackExpectationDigest {
|
||||
return validationError("guarded mutation result does not match leased template, target, guard, digest, or readback identity")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSCUMParsedLogBatchResultForJob(job domain.Job, result domain.SCUMParsedLogBatchResult) error {
|
||||
if err := validator.ValidateSCUMParsedLogBatchResult(result); err != nil {
|
||||
return err
|
||||
}
|
||||
expected := job.ExecutionInput.LogSource
|
||||
if expected == nil {
|
||||
return validationError("parsed log batch request is missing from leased job")
|
||||
}
|
||||
if result.JobID != job.ID {
|
||||
return validationError("parsed log batch result does not match leased job identity")
|
||||
}
|
||||
if result.Binding.ServerInstanceID != job.ServerInstanceID || result.Binding.RunEndpointID != job.RunEndpointID {
|
||||
return validationError("parsed log batch result does not match leased server or Run endpoint")
|
||||
}
|
||||
if result.SourceKey != expected.Key || result.StreamKey != expected.StreamKey {
|
||||
return validationError("parsed log batch result does not match leased log source identity")
|
||||
}
|
||||
for key, value := range map[string]string{
|
||||
"parserKey": result.ParserKey,
|
||||
"parserVersion": result.ParserVersion,
|
||||
"parserDigest": result.ParserDigest,
|
||||
"adapterVersion": result.AdapterVersion,
|
||||
} {
|
||||
if expectedValue := strings.TrimSpace(job.ExecutionInput.Inputs[key]); expectedValue != "" && value != expectedValue {
|
||||
return validationError("parsed log batch result does not match leased parser identity")
|
||||
}
|
||||
}
|
||||
if job.ExecutionInput.PluginID != "" && result.Binding.PluginID != job.ExecutionInput.PluginID {
|
||||
return validationError("parsed log batch result does not match leased plugin identity")
|
||||
}
|
||||
if job.ExecutionInput.TargetVersion != "" && result.Binding.PluginVersion != job.ExecutionInput.TargetVersion {
|
||||
return validationError("parsed log batch result does not match leased plugin version")
|
||||
}
|
||||
if result.FirstCursor.SourceIdentityDigest != result.LastCursor.SourceIdentityDigest || result.FirstCursor.StreamGeneration != result.LastCursor.StreamGeneration {
|
||||
return validationError("parsed log batch result crosses source identity or generation boundaries")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sameSCUMSchemaProbeBinding(a, b domain.SCUMBindingIdentity) bool {
|
||||
return a.ServerInstanceID == b.ServerInstanceID && a.RunBindingID == b.RunBindingID && a.RunEndpointID == b.RunEndpointID && a.PluginID == b.PluginID && a.PluginVersion == b.PluginVersion && a.AdapterVersion == b.AdapterVersion && a.GameVersion == b.GameVersion && a.DatabaseIdentity == b.DatabaseIdentity
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/repo"
|
||||
)
|
||||
|
||||
func TestSCUMCapabilityNegotiationEvaluatesActiveBindingEvidenceIndependently(t *testing.T) {
|
||||
store := repo.NewMemoryStore()
|
||||
svc := NewCoreService(store)
|
||||
now := time.Date(2026, 8, 13, 7, 0, 0, 0, time.UTC)
|
||||
svc.now = func() time.Time { return now }
|
||||
sessionID := seedSCUMCapabilityNegotiationFixture(t, svc, store)
|
||||
binding := scumCapabilityNegotiationBinding()
|
||||
schema := scumNegotiationDigest("a")
|
||||
playerAsset := scumNegotiationDigest("b")
|
||||
|
||||
if err := store.Jobs().Create(domain.Job{ID: "job-scum-probe", ServerInstanceID: binding.ServerInstanceID, RunEndpointID: binding.RunEndpointID, State: domain.JobStateSucceeded, ExecutionResult: domain.JobExecutionResult{Kind: scumSchemaProbeExecutionKind, SQLiteSchemaProbe: &domain.SCUMSchemaProbeResult{RequestID: "job-scum-probe", JobID: "job-scum-probe", Binding: binding, Status: domain.SCUMSchemaProbeStatusSucceeded, SourceFingerprint: scumNegotiationDigest("c"), SchemaFingerprint: schema, ObservedAt: now.Add(-2 * time.Minute), ResultDigest: scumNegotiationDigest("d"), Limits: domain.DefaultSCUMSchemaProbeBounds(), SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}}}); err != nil {
|
||||
t.Fatalf("create probe evidence job: %v", err)
|
||||
}
|
||||
if err := store.Jobs().Create(domain.Job{ID: "job-scum-player-query", ServerInstanceID: binding.ServerInstanceID, RunEndpointID: binding.RunEndpointID, State: domain.JobStateSucceeded, ExecutionResult: domain.JobExecutionResult{Kind: scumSQLiteTemplateExecutionKind, SQLiteTemplate: &domain.SCUMSQLiteTemplateResult{RequestID: "job-scum-player-query", JobID: "job-scum-player-query", Binding: binding, Status: domain.SCUMTerminalResultSucceeded, Capability: domain.SCUMDataCapabilityPlayerRead, TargetKey: "scum-database", TemplateKey: "players-read", AdapterVersion: binding.AdapterVersion, SchemaFingerprint: schema, AssetDigest: playerAsset, ParameterDigest: scumNegotiationDigest("e"), SourceFingerprint: scumNegotiationDigest("f"), ObservedAt: now.Add(-time.Minute), ResultDigest: scumNegotiationDigest("1"), RowCount: 0, Limits: domain.DefaultSCUMSQLiteTemplateBounds(), SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}}}); err != nil {
|
||||
t.Fatalf("create player evidence job: %v", err)
|
||||
}
|
||||
|
||||
negotiation, err := svc.NegotiateSCUMCapabilitiesForSession(sessionID, binding.ServerInstanceID)
|
||||
if err != nil {
|
||||
t.Fatalf("negotiate capabilities: %v", err)
|
||||
}
|
||||
gates := map[domain.SCUMDataCapability]domain.SCUMCapabilityGate{}
|
||||
for _, gate := range negotiation.Gates {
|
||||
gates[gate.Capability] = gate
|
||||
}
|
||||
if !negotiation.ProbeExecutorAvailable || negotiation.RunBindingID != binding.RunBindingID || negotiation.DatabaseIdentity != binding.DatabaseIdentity {
|
||||
t.Fatalf("unexpected negotiation identity: %+v", negotiation)
|
||||
}
|
||||
if gate := gates[domain.SCUMDataCapabilitySchemaProbe]; !gate.Enabled || gate.ReasonCode != domain.SCUMSafeErrorNone {
|
||||
t.Fatalf("schema probe should be enabled from accepted probe evidence, got %+v", gate)
|
||||
}
|
||||
if gate := gates[domain.SCUMDataCapabilityPlayerRead]; !gate.Enabled || gate.ReasonCode != domain.SCUMSafeErrorNone {
|
||||
t.Fatalf("players.read should be enabled from matching template evidence, got %+v", gate)
|
||||
}
|
||||
if gate := gates[domain.SCUMDataCapabilitySquadRead]; gate.Enabled || gate.ReasonCode != domain.SCUMSafeErrorProbeMissing {
|
||||
t.Fatalf("squads.read should remain independently disabled without template evidence, got %+v", gate)
|
||||
}
|
||||
if gate := gates[domain.SCUMDataCapabilityEconomyCommand]; gate.Enabled || gate.ReasonCode != domain.SCUMSafeErrorProbeExecutorAbsent {
|
||||
t.Fatalf("economy-command.write should remain disabled when Run lacks protected RCON, got %+v", gate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMCapabilityNegotiationRejectsEvidenceFromAnotherBinding(t *testing.T) {
|
||||
store := repo.NewMemoryStore()
|
||||
svc := NewCoreService(store)
|
||||
now := time.Date(2026, 8, 13, 7, 30, 0, 0, time.UTC)
|
||||
svc.now = func() time.Time { return now }
|
||||
sessionID := seedSCUMCapabilityNegotiationFixture(t, svc, store)
|
||||
binding := scumCapabilityNegotiationBinding()
|
||||
stale := binding
|
||||
stale.RunBindingID = "runtime-binding-old"
|
||||
if err := store.Jobs().Create(domain.Job{ID: "job-stale-player-query", ServerInstanceID: binding.ServerInstanceID, RunEndpointID: binding.RunEndpointID, State: domain.JobStateSucceeded, ExecutionResult: domain.JobExecutionResult{Kind: scumSQLiteTemplateExecutionKind, SQLiteTemplate: &domain.SCUMSQLiteTemplateResult{RequestID: "job-stale-player-query", JobID: "job-stale-player-query", Binding: stale, Status: domain.SCUMTerminalResultSucceeded, Capability: domain.SCUMDataCapabilityPlayerRead, TargetKey: "scum-database", TemplateKey: "players-read", AdapterVersion: binding.AdapterVersion, SchemaFingerprint: scumNegotiationDigest("a"), AssetDigest: scumNegotiationDigest("b"), ParameterDigest: scumNegotiationDigest("e"), SourceFingerprint: scumNegotiationDigest("f"), ObservedAt: now, ResultDigest: scumNegotiationDigest("1"), RowCount: 0, Limits: domain.DefaultSCUMSQLiteTemplateBounds(), SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}}}); err != nil {
|
||||
t.Fatalf("create stale evidence job: %v", err)
|
||||
}
|
||||
|
||||
negotiation, err := svc.NegotiateSCUMCapabilitiesForSession(sessionID, binding.ServerInstanceID)
|
||||
if err != nil {
|
||||
t.Fatalf("negotiate capabilities: %v", err)
|
||||
}
|
||||
for _, gate := range negotiation.Gates {
|
||||
if gate.Capability == domain.SCUMDataCapabilityPlayerRead {
|
||||
if gate.Enabled || gate.ReasonCode != domain.SCUMSafeErrorBindingMismatch {
|
||||
t.Fatalf("players.read should reject stale binding evidence, got %+v", gate)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("players.read gate missing")
|
||||
}
|
||||
|
||||
func seedSCUMCapabilityNegotiationFixture(t *testing.T, svc *CoreService, store *repo.MemoryStore) string {
|
||||
t.Helper()
|
||||
if _, err := svc.CreateUser(domain.User{ID: "scum-negotiation-owner", DisplayName: "SCUM Negotiation Owner", Email: "scum-negotiation@example.test", Status: domain.UserStatusActive, Roles: []string{"server-owner"}, PasswordHash: "secret-password"}); err != nil {
|
||||
t.Fatalf("create user: %v", err)
|
||||
}
|
||||
auth, err := svc.LoginUser(domain.UserLogin{Account: "scum-negotiation@example.test", Password: "secret-password"})
|
||||
if err != nil {
|
||||
t.Fatalf("login: %v", err)
|
||||
}
|
||||
plugin := domain.GamePlugin{ID: "game.scum", Version: "1.0.0", Status: domain.GamePluginStatusInstalled, SCUMLiveData: domain.SCUMLiveDataManifest{SchemaVersion: "1", Probe: domain.SCUMSchemaProbeDeclaration{Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, TargetKey: "scum-database", Bounds: domain.DefaultSCUMSchemaProbeBounds()}, CapabilityGates: []domain.SCUMLiveDataCapabilityGateDeclaration{
|
||||
{Capability: domain.SCUMDataCapabilitySchemaProbe, Gate: domain.SCUMCapabilityGateEnabled, AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumNegotiationDigest("a"), EvidenceStatus: domain.SCUMCapabilityEvidenceCompatible, SafeReason: "schema probe evidence is compatible"},
|
||||
{Capability: domain.SCUMDataCapabilityPlayerRead, Gate: domain.SCUMCapabilityGateEnabled, AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumNegotiationDigest("a"), RequiredAssetDigests: []string{scumNegotiationDigest("b")}, EvidenceStatus: domain.SCUMCapabilityEvidenceCompatible, SafeReason: "players query evidence is compatible"},
|
||||
{Capability: domain.SCUMDataCapabilitySquadRead, Gate: domain.SCUMCapabilityGateEnabled, AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumNegotiationDigest("a"), RequiredAssetDigests: []string{scumNegotiationDigest("2")}, EvidenceStatus: domain.SCUMCapabilityEvidenceCompatible, SafeReason: "squad query evidence is compatible"},
|
||||
{Capability: domain.SCUMDataCapabilityEconomyCommand, Gate: domain.SCUMCapabilityGateEnabled, AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumNegotiationDigest("a"), RequiredAssetDigests: []string{scumNegotiationDigest("3")}, EvidenceStatus: domain.SCUMCapabilityEvidenceCompatible, SafeReason: "economy command evidence is compatible"},
|
||||
{Capability: domain.SCUMDataCapabilityGiftCommand, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumNegotiationDigest("a"), EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "gift command evidence is missing"},
|
||||
{Capability: domain.SCUMDataCapabilityProfileXMLWrite, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumNegotiationDigest("a"), EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "XML mutation evidence is missing"},
|
||||
}}, RuntimeProfiles: domain.GamePluginRuntimeProfiles{LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process"}}}}
|
||||
if err := store.GamePlugins().Create(plugin); err != nil {
|
||||
t.Fatalf("create plugin: %v", err)
|
||||
}
|
||||
endpoint := domain.RunEndpoint{ID: "run-scum-negotiation", DisplayName: "Run SCUM Negotiation", Version: "0.1.0", Status: domain.RunEndpointStatusOnline, Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe, domain.JobCapabilityRemoteRunDBSQLiteQuery}}
|
||||
if err := store.RunEndpoints().Create(endpoint); err != nil {
|
||||
t.Fatalf("create endpoint: %v", err)
|
||||
}
|
||||
if err := store.ServerInstances().Create(domain.ServerInstance{ID: "server-scum-negotiation", PluginID: plugin.ID, PluginVersion: plugin.Version, RunEndpointID: endpoint.ID, Name: "SCUM Negotiation", OwnerUserID: "scum-negotiation-owner", State: domain.ServerInstanceStateRunning}); err != nil {
|
||||
t.Fatalf("create server: %v", err)
|
||||
}
|
||||
if _, err := svc.UpdateServerRuntimeBindingForSession(auth.SessionID, "server-scum-negotiation", domain.RuntimeBindingUpdate{ProfileKey: "local", Bindings: map[string]string{}}); err != nil {
|
||||
t.Fatalf("create runtime binding: %v", err)
|
||||
}
|
||||
return auth.SessionID
|
||||
}
|
||||
|
||||
func scumCapabilityNegotiationBinding() domain.SCUMBindingIdentity {
|
||||
return domain.SCUMBindingIdentity{ServerInstanceID: "server-scum-negotiation", RunBindingID: "runtime-binding-server-scum-negotiation", RunEndpointID: "run-scum-negotiation", PluginID: "game.scum", PluginVersion: "1.0.0", AdapterVersion: "adapter-1", DatabaseIdentity: "scum-database"}
|
||||
}
|
||||
|
||||
func scumNegotiationDigest(char string) string {
|
||||
return "sha256:" + strings.Repeat(char, 64)
|
||||
}
|
||||
@@ -0,0 +1,770 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/repo"
|
||||
)
|
||||
|
||||
type scumSQLiteMutationJobResult struct {
|
||||
Outcome string `json:"outcome"`
|
||||
AffectedRows int `json:"affectedRows"`
|
||||
MutationChecksum string `json:"mutationChecksum"`
|
||||
ConfirmationRows []map[string]any `json:"confirmationRows"`
|
||||
SafeMessage string `json:"safeMessage"`
|
||||
}
|
||||
|
||||
func (svc *CoreService) RequestSCUMOperationForSession(sessionID, serverID string, request domain.SCUMOperationRequest) (domain.SCUMOperationRequest, error) {
|
||||
request = domain.CopySCUMOperationRequest(request)
|
||||
user, err := svc.GetCurrentUser(sessionID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
if err := svc.authorizeServerLifecycle(sessionID, serverID); err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(serverID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
template, ok := scumOperationTemplate(plugin, request.TemplateKey)
|
||||
if !ok {
|
||||
return domain.SCUMOperationRequest{}, validationError("SCUM operation template is not declared")
|
||||
}
|
||||
if !containsString(plugin.DeclaredPermissions, template.Permission) {
|
||||
return domain.SCUMOperationRequest{}, validationError("SCUM operation permission is not declared")
|
||||
}
|
||||
if strings.TrimSpace(request.IdempotencyKey) == "" || len(request.IdempotencyKey) > 120 {
|
||||
return domain.SCUMOperationRequest{}, validationError("operation idempotency key is required")
|
||||
}
|
||||
existing, err := svc.store.SCUMOperationRequests().List(domain.SCUMOperationRequestFilter{ServerInstanceID: serverID, IdempotencyKey: request.IdempotencyKey})
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
if len(existing) > 0 {
|
||||
return domain.CopySCUMOperationRequest(existing[0]), nil
|
||||
}
|
||||
playerID := coalesceString(request.PlayerID, firstString(request.Payload, "playerId", "steamId"))
|
||||
if playerID == "" && request.TemplateKey != "server.reward.command.deliver" {
|
||||
return domain.SCUMOperationRequest{}, validationError("operation playerId is required")
|
||||
}
|
||||
summary := operationSafeSummary(request.TemplateKey, playerID, request.Payload)
|
||||
switch template.Kind {
|
||||
case domain.GameClientBridgeOperationKindRCON:
|
||||
if err := validateSCUMRCONOperationPayload(request.TemplateKey, playerID, request.Payload); err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
case domain.GameClientBridgeOperationKindSQLiteMutation:
|
||||
guard, payload, err := normalizeSCUMSQLiteMutationRequest(template, playerID, request.Payload, request.Guard)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
request.Guard = guard
|
||||
request.Payload = payload
|
||||
summary = scumSQLiteMutationSafeSummary(request.TemplateKey, playerID, guard)
|
||||
default:
|
||||
return domain.SCUMOperationRequest{}, validationError("SCUM operation kind is unsupported")
|
||||
}
|
||||
stamp := svc.now()
|
||||
operation := domain.SCUMOperationRequest{ID: "scum-operation-" + fingerprintID(serverID, request.IdempotencyKey), ServerInstanceID: serverID, PluginID: instance.PluginID, TemplateKey: request.TemplateKey, PlayerID: playerID, RequesterID: user.ID, ApprovalLevel: template.ApprovalLevel, Payload: domain.CopyGameClientBridgePayload(request.Payload), Guard: request.Guard, Status: domain.SCUMWorkflowStepWaiting, Reason: bounded(request.Reason, 240), IdempotencyKey: request.IdempotencyKey, SafeSummary: summary, CreatedAt: stamp, UpdatedAt: stamp}
|
||||
if err := svc.store.SCUMOperationRequests().Create(operation); err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
_, err = svc.recordAuditEventWithID(user.ID, "scum.operation.request", "scum-operation", operation.ID, domain.AuditResultQueued, "typed SCUM operation awaiting approval")
|
||||
return domain.CopySCUMOperationRequest(operation), err
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMOperationsForSession(sessionID string, filter domain.SCUMOperationRequestFilter) ([]domain.SCUMOperationRequest, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := svc.store.SCUMOperationRequests().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitSCUMProjectionSlice(&values, filter.Limit)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ApproveSCUMOperationForSession(sessionID, operationID string) (domain.SCUMOperationRequest, error) {
|
||||
operation, err := svc.store.SCUMOperationRequests().Get(operationID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
user, err := svc.GetCurrentUser(sessionID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
if err := svc.authorizeServerLifecycle(sessionID, operation.ServerInstanceID); err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
if operation.ApprovalLevel == domain.GameClientBridgeApprovalLevelPlatformAdmin && !isPlatformAdmin(user) {
|
||||
return domain.SCUMOperationRequest{}, ErrForbidden
|
||||
}
|
||||
if operation.Status != domain.SCUMWorkflowStepWaiting {
|
||||
return domain.SCUMOperationRequest{}, validationError("SCUM operation is not awaiting approval")
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(operation.ServerInstanceID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
template, ok := scumOperationTemplate(plugin, operation.TemplateKey)
|
||||
if !ok {
|
||||
return domain.SCUMOperationRequest{}, validationError("SCUM operation template is not declared")
|
||||
}
|
||||
var jobID string
|
||||
var auditSummary string
|
||||
switch template.Kind {
|
||||
case domain.GameClientBridgeOperationKindRCON:
|
||||
request, err := svc.sourceRCONRequestForSCUMOperation(operation)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
dispatch, err := svc.DispatchSourceRCONCommandForSession(sessionID, request)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
jobID = dispatch.JobID
|
||||
auditSummary = "typed SCUM operation dispatched through transient RCON input"
|
||||
case domain.GameClientBridgeOperationKindSQLiteMutation:
|
||||
gated, ready, err := svc.applySCUMSQLiteMutationApprovalGate(operation, template)
|
||||
if err != nil || !ready {
|
||||
return gated, err
|
||||
}
|
||||
operation = gated
|
||||
job, err := svc.dispatchSCUMSQLiteMutationOperation(operation, template)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
jobID = job.ID
|
||||
auditSummary = "typed SCUM DB mutation dispatched through template-bound Run job"
|
||||
default:
|
||||
return domain.SCUMOperationRequest{}, validationError("SCUM operation kind is unsupported")
|
||||
}
|
||||
stamp := svc.now()
|
||||
operation.ApproverID = user.ID
|
||||
operation.ApprovedAt = stamp
|
||||
operation.Status = domain.SCUMWorkflowStepQueued
|
||||
operation.RunJobID = jobID
|
||||
operation.UpdatedAt = stamp
|
||||
operation.AuditReferences = append(operation.AuditReferences, "job:"+jobID)
|
||||
if err := svc.store.SCUMOperationRequests().Update(operation); err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
_, err = svc.recordAuditEventWithID(user.ID, "scum.operation.approve", "scum-operation", operation.ID, domain.AuditResultQueued, auditSummary)
|
||||
return domain.CopySCUMOperationRequest(operation), err
|
||||
}
|
||||
|
||||
func (svc *CoreService) ReconcileSCUMOperation(operationID string) (domain.SCUMOperationRequest, error) {
|
||||
operation, err := svc.store.SCUMOperationRequests().Get(operationID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
if strings.TrimSpace(operation.RunJobID) == "" {
|
||||
return domain.CopySCUMOperationRequest(operation), nil
|
||||
}
|
||||
job, err := svc.store.Jobs().Get(operation.RunJobID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(operation.ServerInstanceID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
template, _ := scumOperationTemplate(plugin, operation.TemplateKey)
|
||||
stamp := svc.now()
|
||||
switch job.State {
|
||||
case domain.JobStateSucceeded:
|
||||
if template.Kind == domain.GameClientBridgeOperationKindSQLiteMutation {
|
||||
if updated, terminal := reconcileSCUMSQLiteMutationJobResult(operation, template, job); terminal {
|
||||
operation = updated
|
||||
} else {
|
||||
operation = updated
|
||||
operation.Status = domain.SCUMWorkflowStepConfirming
|
||||
}
|
||||
} else if operation.Confirmation.Status == "confirmed" {
|
||||
operation.Status = domain.SCUMWorkflowStepConfirmed
|
||||
} else {
|
||||
operation.Status = domain.SCUMWorkflowStepConfirming
|
||||
}
|
||||
case domain.JobStateFailed:
|
||||
if strings.Contains(strings.ToLower(job.ExecutionResult.Kind), "unknown") || strings.Contains(strings.ToLower(job.ExecutionResult.AuditSummary), "unknown") {
|
||||
operation.Status = domain.SCUMWorkflowStepUnknown
|
||||
} else {
|
||||
operation.Status = domain.SCUMWorkflowStepFailed
|
||||
}
|
||||
operation.CompletedAt = stamp
|
||||
case domain.JobStateCancelled:
|
||||
operation.Status = domain.SCUMWorkflowStepUnknown
|
||||
operation.CompletedAt = stamp
|
||||
}
|
||||
operation.UpdatedAt = stamp
|
||||
if (operation.Status == domain.SCUMWorkflowStepConfirmed || operation.Status == domain.SCUMWorkflowStepFailed || operation.Status == domain.SCUMWorkflowStepUnknown) && operation.CompletedAt.IsZero() {
|
||||
operation.CompletedAt = stamp
|
||||
}
|
||||
if err := svc.store.SCUMOperationRequests().Update(operation); err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
return domain.CopySCUMOperationRequest(operation), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ConfirmSCUMOperation(operationID string, confirmation domain.SCUMOperationConfirmation) (domain.SCUMOperationRequest, error) {
|
||||
operation, err := svc.store.SCUMOperationRequests().Get(operationID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(operation.ServerInstanceID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
template, _ := scumOperationTemplate(plugin, operation.TemplateKey)
|
||||
confirmation = domain.CopySCUMOperationConfirmation(confirmation)
|
||||
stamp := svc.now()
|
||||
if confirmation.Status != "confirmed" {
|
||||
operation.Status = domain.SCUMWorkflowStepFailed
|
||||
operation.Confirmation = confirmation
|
||||
operation.CompletedAt = stamp
|
||||
operation.UpdatedAt = stamp
|
||||
if err := svc.store.SCUMOperationRequests().Update(operation); err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
return domain.CopySCUMOperationRequest(operation), nil
|
||||
}
|
||||
if template.Kind == domain.GameClientBridgeOperationKindSQLiteMutation && !scumSQLiteMutationConfirmationMatches(operation, confirmation.ConfirmedFields) {
|
||||
confirmation.Status = "failed"
|
||||
confirmation.SafeSummary = domain.SCUMSafeSummary{Title: "DB mutation confirmation mismatch", Message: "Run readback did not prove the requested SCUM player field value."}
|
||||
operation.Status = domain.SCUMWorkflowStepFailed
|
||||
operation.Confirmation = confirmation
|
||||
operation.CompletedAt = stamp
|
||||
operation.UpdatedAt = stamp
|
||||
if err := svc.store.SCUMOperationRequests().Update(operation); err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
return domain.CopySCUMOperationRequest(operation), nil
|
||||
}
|
||||
operation.Confirmation = confirmation
|
||||
operation.Status = domain.SCUMWorkflowStepConfirmed
|
||||
operation.CompletedAt = stamp
|
||||
operation.UpdatedAt = stamp
|
||||
if err := svc.store.SCUMOperationRequests().Update(operation); err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
return domain.CopySCUMOperationRequest(operation), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) sourceRCONRequestForSCUMOperation(operation domain.SCUMOperationRequest) (domain.SourceRCONCommandRequest, error) {
|
||||
command, chat, err := scumRCONCommandForOperation(operation)
|
||||
if err != nil {
|
||||
return domain.SourceRCONCommandRequest{}, err
|
||||
}
|
||||
request := domain.SourceRCONCommandRequest{ServerInstanceID: operation.ServerInstanceID, IdempotencyKey: "scum-operation-" + operation.IdempotencyKey}
|
||||
if chat != "" {
|
||||
request.Kind = domain.SourceRCONCommandKindChat
|
||||
request.ChatType = 4
|
||||
request.TargetSteamID = operation.PlayerID
|
||||
request.Message = chat
|
||||
return request, nil
|
||||
}
|
||||
request.Kind = domain.SourceRCONCommandKindCommand
|
||||
request.Command = command
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func scumRCONCommandForOperation(operation domain.SCUMOperationRequest) (command string, chat string, err error) {
|
||||
playerID := operation.PlayerID
|
||||
switch operation.TemplateKey {
|
||||
case "player.fame.set":
|
||||
amount, ok := operationInteger(operation.Payload, "fame", "amount", "value")
|
||||
if !ok {
|
||||
return "", "", validationError("fame amount is required")
|
||||
}
|
||||
return fmt.Sprintf("#SetFamePoints %d %q", amount, playerID), "", nil
|
||||
case "player.currency.normal.set":
|
||||
amount, ok := operationInteger(operation.Payload, "amount", "balance", "normalBalance")
|
||||
if !ok {
|
||||
return "", "", validationError("normal currency amount is required")
|
||||
}
|
||||
return fmt.Sprintf("#SetCurrencyBalance Normal %d %q", amount, playerID), "", nil
|
||||
case "player.currency.gold.set":
|
||||
amount, ok := operationInteger(operation.Payload, "amount", "balance", "goldBalance")
|
||||
if !ok {
|
||||
return "", "", validationError("gold currency amount is required")
|
||||
}
|
||||
return fmt.Sprintf("#SetCurrencyBalance Gold %d %q", amount, playerID), "", nil
|
||||
case "player.notify":
|
||||
message := strings.TrimSpace(firstString(operation.Payload, "message", "notice"))
|
||||
if message == "" || len(message) > 200 {
|
||||
return "", "", validationError("notification message is required")
|
||||
}
|
||||
return "", message, nil
|
||||
default:
|
||||
return "", "", validationError("unsupported SCUM RCON operation template")
|
||||
}
|
||||
}
|
||||
|
||||
func validateSCUMRCONOperationPayload(templateKey, playerID string, payload map[string]any) error {
|
||||
operation := domain.SCUMOperationRequest{TemplateKey: templateKey, PlayerID: playerID, Payload: payload}
|
||||
command, chat, err := scumRCONCommandForOperation(operation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.ContainsAny(command, "\r\n") || strings.ContainsAny(chat, "\r\n") {
|
||||
return validationError("operation payload contains invalid control characters")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeSCUMSQLiteMutationRequest(template domain.GameClientBridgeOperationTemplateDeclaration, playerID string, payload map[string]any, guard domain.SCUMMutationGuard) (domain.SCUMMutationGuard, map[string]any, error) {
|
||||
lowerKey := strings.ToLower(template.Key)
|
||||
if strings.Contains(lowerKey, "fame") || strings.Contains(lowerKey, "currency") {
|
||||
return domain.SCUMMutationGuard{}, nil, validationError("SCUM fame and currency edits must use RCON operation templates")
|
||||
}
|
||||
if template.ApprovalLevel != domain.GameClientBridgeApprovalLevelPlatformAdmin {
|
||||
return domain.SCUMMutationGuard{}, nil, validationError("SCUM DB mutation requires platform-admin approval")
|
||||
}
|
||||
if template.Mutation.FieldKey == "" || template.Mutation.ConfirmationQueryKey == "" || template.Mutation.TableKey == "" || template.Mutation.IdentityKey == "" || template.Mutation.ValueKey == "" {
|
||||
return domain.SCUMMutationGuard{}, nil, validationError("SCUM DB mutation metadata is incomplete")
|
||||
}
|
||||
if template.MaxRowsAffected < 1 {
|
||||
return domain.SCUMMutationGuard{}, nil, validationError("SCUM DB mutation row bound is required")
|
||||
}
|
||||
payload = domain.CopyGameClientBridgePayload(payload)
|
||||
if guard.FieldKey == "" {
|
||||
guard.FieldKey = coalesceString(firstString(payload, "fieldKey"), template.Mutation.FieldKey)
|
||||
}
|
||||
if guard.Before == nil {
|
||||
guard.Before = payload["before"]
|
||||
}
|
||||
if guard.After == nil {
|
||||
guard.After = payload["after"]
|
||||
if guard.After == nil {
|
||||
guard.After = payload["value"]
|
||||
}
|
||||
}
|
||||
if guard.MaxRowsAffected == 0 {
|
||||
guard.MaxRowsAffected = template.MaxRowsAffected
|
||||
}
|
||||
guard.SafetyWindow = coalesceString(guard.SafetyWindow, firstString(payload, "safetyWindow", "maintenanceWindow"))
|
||||
guard.BackupRef = coalesceString(guard.BackupRef, firstString(payload, "backupRef", "snapshotRef"))
|
||||
guard.RequiresOfflinePlayer = template.Safety.RequiresOfflinePlayer
|
||||
guard.RequiresMaintenance = template.Safety.RequiresMaintenanceWindow
|
||||
guard.RequiresBackup = template.Safety.BackupRequired
|
||||
if playerID == "" {
|
||||
return domain.SCUMMutationGuard{}, nil, validationError("SCUM DB mutation playerId is required")
|
||||
}
|
||||
if guard.FieldKey != template.Mutation.FieldKey {
|
||||
return domain.SCUMMutationGuard{}, nil, validationError("SCUM DB mutation field key does not match template")
|
||||
}
|
||||
if guard.Before == nil || guard.After == nil {
|
||||
return domain.SCUMMutationGuard{}, nil, validationError("SCUM DB mutation before and after values are required")
|
||||
}
|
||||
if guard.MaxRowsAffected < 1 || guard.MaxRowsAffected > template.MaxRowsAffected {
|
||||
return domain.SCUMMutationGuard{}, nil, validationError("SCUM DB mutation maxRowsAffected exceeds template bound")
|
||||
}
|
||||
if err := validateSCUMMutationValue(template, guard.Before, "before"); err != nil {
|
||||
return domain.SCUMMutationGuard{}, nil, err
|
||||
}
|
||||
if err := validateSCUMMutationValue(template, guard.After, "after"); err != nil {
|
||||
return domain.SCUMMutationGuard{}, nil, err
|
||||
}
|
||||
for key, value := range map[string]any{"playerId": playerID, "fieldKey": guard.FieldKey, "before": guard.Before, "after": guard.After, "safetyWindow": guard.SafetyWindow, "backupRef": guard.BackupRef} {
|
||||
if value != nil && value != "" {
|
||||
payload[key] = value
|
||||
}
|
||||
}
|
||||
return guard, payload, nil
|
||||
}
|
||||
|
||||
func validateSCUMMutationValue(template domain.GameClientBridgeOperationTemplateDeclaration, value any, label string) error {
|
||||
switch template.Mutation.AllowedValueType {
|
||||
case "integer":
|
||||
parsed, ok := anyInt64(value)
|
||||
if !ok {
|
||||
return validationError("SCUM DB mutation " + label + " value must be an integer")
|
||||
}
|
||||
if template.Mutation.MinValue != 0 && float64(parsed) < template.Mutation.MinValue || template.Mutation.MaxValue != 0 && float64(parsed) > template.Mutation.MaxValue {
|
||||
return validationError("SCUM DB mutation " + label + " value is outside the template range")
|
||||
}
|
||||
case "number":
|
||||
parsed, ok := anyFloat64(value)
|
||||
if !ok {
|
||||
return validationError("SCUM DB mutation " + label + " value must be numeric")
|
||||
}
|
||||
if template.Mutation.MinValue != 0 && parsed < template.Mutation.MinValue || template.Mutation.MaxValue != 0 && parsed > template.Mutation.MaxValue {
|
||||
return validationError("SCUM DB mutation " + label + " value is outside the template range")
|
||||
}
|
||||
case "string":
|
||||
if strings.TrimSpace(fmt.Sprint(value)) == "" || strings.ContainsAny(fmt.Sprint(value), "\r\n") {
|
||||
return validationError("SCUM DB mutation " + label + " value is invalid")
|
||||
}
|
||||
case "boolean":
|
||||
if _, ok := value.(bool); !ok {
|
||||
return validationError("SCUM DB mutation " + label + " value must be boolean")
|
||||
}
|
||||
default:
|
||||
return validationError("SCUM DB mutation value type is unsupported")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) applySCUMSQLiteMutationApprovalGate(operation domain.SCUMOperationRequest, template domain.GameClientBridgeOperationTemplateDeclaration) (domain.SCUMOperationRequest, bool, error) {
|
||||
state, err := svc.latestSCUMPlayerLiveState(operation.ServerInstanceID, operation.PlayerID)
|
||||
if err != nil {
|
||||
if err == repo.ErrNotFound {
|
||||
return svc.updateSCUMOperationGate(operation, domain.SCUMWorkflowStepWaiting, "等待真实玩家投影", "需要先从当前服务的登录日志或 SCUM.db 读取玩家数据。")
|
||||
}
|
||||
return domain.SCUMOperationRequest{}, false, err
|
||||
}
|
||||
if state.Freshness.Status != domain.SCUMProjectionFresh {
|
||||
return svc.updateSCUMOperationGate(operation, domain.SCUMWorkflowStepWaiting, "等待新鲜投影", "玩家投影不是 fresh,需先刷新 SCUM.db/readback。")
|
||||
}
|
||||
if template.Safety.RequiresOfflinePlayer && state.Online {
|
||||
return svc.updateSCUMOperationGate(operation, domain.SCUMWorkflowStepWaiting, "等待玩家离线", "DB-only 玩家字段修改必须等玩家离线或进入维护窗口。")
|
||||
}
|
||||
if template.Safety.RequiresMaintenanceWindow && strings.TrimSpace(operation.Guard.SafetyWindow) == "" {
|
||||
return svc.updateSCUMOperationGate(operation, domain.SCUMWorkflowStepWaiting, "缺少维护窗口", "DB mutation 需要记录维护窗口/离线安全证据。")
|
||||
}
|
||||
if template.Safety.BackupRequired && strings.TrimSpace(operation.Guard.BackupRef) == "" {
|
||||
return svc.updateSCUMOperationGate(operation, domain.SCUMWorkflowStepWaiting, "缺少备份快照", "DB mutation 需要 run 或管理员提供 backup/snapshot evidence。")
|
||||
}
|
||||
current, ok := scumCurrentMutationFieldValue(state, operation.Guard.FieldKey)
|
||||
if !ok {
|
||||
return svc.updateSCUMOperationGate(operation, domain.SCUMWorkflowStepWaiting, "等待字段读回", "当前投影没有该 DB-only 字段,需先执行确认查询。")
|
||||
}
|
||||
if !scumScalarEqual(current, operation.Guard.Before) {
|
||||
return svc.updateSCUMOperationGate(operation, domain.SCUMWorkflowStepBlocked, "before value 已过期", "当前投影值与审批时 before guard 不一致,已阻止写入。")
|
||||
}
|
||||
return domain.CopySCUMOperationRequest(operation), true, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) updateSCUMOperationGate(operation domain.SCUMOperationRequest, status domain.SCUMWorkflowStepStatus, title string, message string) (domain.SCUMOperationRequest, bool, error) {
|
||||
operation.Status = status
|
||||
operation.SafeSummary = domain.SCUMSafeSummary{Title: title, Message: message, Details: map[string]string{"template": operation.TemplateKey, "playerId": operation.PlayerID}}
|
||||
operation.UpdatedAt = svc.now()
|
||||
if err := svc.store.SCUMOperationRequests().Update(operation); err != nil {
|
||||
return domain.SCUMOperationRequest{}, false, err
|
||||
}
|
||||
return domain.CopySCUMOperationRequest(operation), false, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) latestSCUMPlayerLiveState(serverID, playerID string) (domain.SCUMPlayerLiveState, error) {
|
||||
states, err := svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: serverID, GamePlayerID: playerID})
|
||||
if err != nil {
|
||||
return domain.SCUMPlayerLiveState{}, err
|
||||
}
|
||||
if len(states) == 0 {
|
||||
states, err = svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: serverID, SteamID: playerID})
|
||||
if err != nil {
|
||||
return domain.SCUMPlayerLiveState{}, err
|
||||
}
|
||||
}
|
||||
if len(states) == 0 {
|
||||
return domain.SCUMPlayerLiveState{}, repo.ErrNotFound
|
||||
}
|
||||
best := states[0]
|
||||
for _, state := range states[1:] {
|
||||
if state.Freshness.ObservedAt.After(best.Freshness.ObservedAt) || state.UpdatedAt.After(best.UpdatedAt) {
|
||||
best = state
|
||||
}
|
||||
}
|
||||
return domain.CopySCUMPlayerLiveState(best), nil
|
||||
}
|
||||
|
||||
func scumCurrentMutationFieldValue(state domain.SCUMPlayerLiveState, fieldKey string) (any, bool) {
|
||||
if state.UnknownFields != nil {
|
||||
for _, key := range []string{fieldKey, "field" + fieldKey, "attribute" + fieldKey, "attribute_" + fieldKey, "stat" + fieldKey, "stat_" + fieldKey} {
|
||||
if value, ok := state.UnknownFields[key]; ok {
|
||||
return value, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (svc *CoreService) dispatchSCUMSQLiteMutationOperation(operation domain.SCUMOperationRequest, template domain.GameClientBridgeOperationTemplateDeclaration) (domain.Job, error) {
|
||||
instance, err := svc.store.ServerInstances().Get(operation.ServerInstanceID)
|
||||
if err != nil {
|
||||
return domain.Job{}, err
|
||||
}
|
||||
jobID := jobIDFromParts("job-scum-sqlite-mutation", instance.ID, operation.IdempotencyKey)
|
||||
job := domain.Job{ID: jobID, ServerInstanceID: instance.ID, RunEndpointID: instance.RunEndpointID, Capability: domain.JobCapabilityRemoteRunProtectedSQL, TargetKey: template.TargetKey, InputRef: "input://scum-operation/" + operation.ID, IdempotencyKey: "scum-sqlite-mutation:" + operation.IdempotencyKey, Progress: domain.JobProgress{Percent: 0, Message: "typed SCUM DB mutation queued"}, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, ExecutionInput: domain.JobExecutionInput{WorkspaceScope: svc.runtimeProfileScope(instance.ID), RemoteAdapterKey: template.TransportKey, RemoteAdapterKind: "protected-sql", TimeoutSeconds: template.TimeoutSeconds, PluginID: operation.PluginID, Inputs: scumSQLiteMutationJobInputs(operation, template)}}
|
||||
created, err := svc.CreateJob(job)
|
||||
if err != nil {
|
||||
return domain.Job{}, err
|
||||
}
|
||||
if created.ID != jobID || created.Capability != domain.JobCapabilityRemoteRunProtectedSQL || created.TargetKey != template.TargetKey || created.ExecutionInput.RemoteAdapterKey != template.TransportKey {
|
||||
return domain.Job{}, validationError("SCUM DB mutation idempotency key is already bound")
|
||||
}
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func scumSQLiteMutationJobInputs(operation domain.SCUMOperationRequest, template domain.GameClientBridgeOperationTemplateDeclaration) map[string]string {
|
||||
return map[string]string{
|
||||
"operationId": operation.ID,
|
||||
"templateKey": operation.TemplateKey,
|
||||
"playerId": operation.PlayerID,
|
||||
"fieldKey": operation.Guard.FieldKey,
|
||||
"tableKey": template.Mutation.TableKey,
|
||||
"identityKey": template.Mutation.IdentityKey,
|
||||
"valueKey": template.Mutation.ValueKey,
|
||||
"before": scumScalarString(operation.Guard.Before),
|
||||
"after": scumScalarString(operation.Guard.After),
|
||||
"maxRowsAffected": strconv.Itoa(operation.Guard.MaxRowsAffected),
|
||||
"confirmationQueryKey": template.Mutation.ConfirmationQueryKey,
|
||||
"safetyWindow": operation.Guard.SafetyWindow,
|
||||
"backupRef": operation.Guard.BackupRef,
|
||||
}
|
||||
}
|
||||
|
||||
func reconcileSCUMSQLiteMutationJobResult(operation domain.SCUMOperationRequest, template domain.GameClientBridgeOperationTemplateDeclaration, job domain.Job) (domain.SCUMOperationRequest, bool) {
|
||||
result, ok := parseSCUMSQLiteMutationJobResult(job.ExecutionResult.Content)
|
||||
if !ok || result.Outcome == "unknown" || strings.Contains(strings.ToLower(job.ExecutionResult.Kind), "unknown") {
|
||||
operation.Status = domain.SCUMWorkflowStepUnknown
|
||||
operation.Confirmation = domain.SCUMOperationConfirmation{Status: "unknown", SafeSummary: domain.SCUMSafeSummary{Title: "DB mutation state unknown", Message: "Run did not return a valid bounded mutation result."}}
|
||||
return operation, true
|
||||
}
|
||||
operation.Confirmation.AffectedRows = result.AffectedRows
|
||||
operation.Confirmation.MutationChecksum = result.MutationChecksum
|
||||
operation.Confirmation.Checksum = coalesceString(operation.Confirmation.Checksum, coalesceString(result.MutationChecksum, job.ExecutionResult.Checksum))
|
||||
if result.Outcome == "stale-before" {
|
||||
operation.Status = domain.SCUMWorkflowStepFailed
|
||||
operation.Confirmation.Status = "failed"
|
||||
operation.SafeSummary = domain.SCUMSafeSummary{Title: "before value 已过期", Message: "Run 在写入前发现当前 DB 值与 approved before guard 不一致。"}
|
||||
return operation, true
|
||||
}
|
||||
if result.Outcome != "succeeded" || result.AffectedRows < 1 {
|
||||
operation.Status = domain.SCUMWorkflowStepFailed
|
||||
operation.Confirmation.Status = "failed"
|
||||
operation.SafeSummary = domain.SCUMSafeSummary{Title: "DB mutation failed", Message: bounded(coalesceString(result.SafeMessage, "Run reported the mutation did not succeed."), 240)}
|
||||
return operation, true
|
||||
}
|
||||
if result.AffectedRows > template.MaxRowsAffected || result.AffectedRows > operation.Guard.MaxRowsAffected || strings.TrimSpace(result.MutationChecksum) == "" {
|
||||
operation.Status = domain.SCUMWorkflowStepUnknown
|
||||
operation.Confirmation.Status = "unknown"
|
||||
operation.SafeSummary = domain.SCUMSafeSummary{Title: "DB mutation row bound unknown", Message: "Run result exceeded declared row bounds or omitted mutation checksum."}
|
||||
return operation, true
|
||||
}
|
||||
if len(result.ConfirmationRows) > 0 {
|
||||
for _, row := range result.ConfirmationRows {
|
||||
if scumSQLiteMutationConfirmationMatches(operation, row) {
|
||||
operation.Status = domain.SCUMWorkflowStepConfirmed
|
||||
operation.Confirmation.Status = "confirmed"
|
||||
operation.Confirmation.ConfirmedFields = domain.CopyGameClientBridgePayload(row)
|
||||
return operation, true
|
||||
}
|
||||
}
|
||||
operation.Status = domain.SCUMWorkflowStepFailed
|
||||
operation.Confirmation.Status = "failed"
|
||||
operation.SafeSummary = domain.SCUMSafeSummary{Title: "DB mutation confirmation mismatch", Message: "Run confirmation rows did not match the requested after value."}
|
||||
return operation, true
|
||||
}
|
||||
operation.Confirmation.Status = "executed"
|
||||
return operation, false
|
||||
}
|
||||
|
||||
func parseSCUMSQLiteMutationJobResult(content string) (scumSQLiteMutationJobResult, bool) {
|
||||
if strings.TrimSpace(content) == "" {
|
||||
return scumSQLiteMutationJobResult{}, false
|
||||
}
|
||||
var result scumSQLiteMutationJobResult
|
||||
if err := json.Unmarshal([]byte(content), &result); err != nil {
|
||||
return scumSQLiteMutationJobResult{}, false
|
||||
}
|
||||
result.Outcome = strings.TrimSpace(result.Outcome)
|
||||
return result, result.Outcome != ""
|
||||
}
|
||||
|
||||
func scumSQLiteMutationConfirmationMatches(operation domain.SCUMOperationRequest, row map[string]any) bool {
|
||||
if row == nil {
|
||||
return false
|
||||
}
|
||||
rowPlayerID := firstString(row, "playerId", "gamePlayerId", "steamId", "steam_id")
|
||||
if rowPlayerID != "" && rowPlayerID != operation.PlayerID {
|
||||
return false
|
||||
}
|
||||
if field := firstString(row, "fieldKey", "field", "attributeKey"); field != "" && field != operation.Guard.FieldKey {
|
||||
return false
|
||||
}
|
||||
for _, key := range []string{"value", "after", operation.Guard.FieldKey, "field" + operation.Guard.FieldKey, "attribute" + operation.Guard.FieldKey, "attribute_" + operation.Guard.FieldKey} {
|
||||
if value, ok := row[key]; ok && scumScalarEqual(value, operation.Guard.After) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func scumSQLiteMutationSafeSummary(templateKey, playerID string, guard domain.SCUMMutationGuard) domain.SCUMSafeSummary {
|
||||
details := map[string]string{"template": templateKey, "fieldKey": guard.FieldKey, "maxRowsAffected": strconv.Itoa(guard.MaxRowsAffected)}
|
||||
if playerID != "" {
|
||||
details["playerId"] = playerID
|
||||
}
|
||||
if guard.SafetyWindow != "" {
|
||||
details["safetyWindow"] = guard.SafetyWindow
|
||||
}
|
||||
if guard.BackupRef != "" {
|
||||
details["backupRef"] = guard.BackupRef
|
||||
}
|
||||
return domain.SCUMSafeSummary{Title: "Typed SCUM DB mutation", Message: "Run executes this through a declared mutation template with before-value and row-bound guards; raw SQL is not stored.", Details: details}
|
||||
}
|
||||
|
||||
func operationInteger(payload map[string]any, keys ...string) (int64, bool) {
|
||||
for _, key := range keys {
|
||||
value, exists := payload[key]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
switch typed := value.(type) {
|
||||
case int:
|
||||
return int64(typed), true
|
||||
case int64:
|
||||
return typed, true
|
||||
case uint64:
|
||||
if typed > uint64(^uint64(0)>>1) {
|
||||
return 0, false
|
||||
}
|
||||
return int64(typed), true
|
||||
case float64:
|
||||
if typed == float64(int64(typed)) {
|
||||
return int64(typed), true
|
||||
}
|
||||
case string:
|
||||
parsed, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
|
||||
if err == nil {
|
||||
return parsed, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func anyInt64(value any) (int64, bool) {
|
||||
switch typed := value.(type) {
|
||||
case int:
|
||||
return int64(typed), true
|
||||
case int8:
|
||||
return int64(typed), true
|
||||
case int16:
|
||||
return int64(typed), true
|
||||
case int32:
|
||||
return int64(typed), true
|
||||
case int64:
|
||||
return typed, true
|
||||
case uint:
|
||||
return int64(typed), true
|
||||
case uint8:
|
||||
return int64(typed), true
|
||||
case uint16:
|
||||
return int64(typed), true
|
||||
case uint32:
|
||||
return int64(typed), true
|
||||
case uint64:
|
||||
if typed > uint64(^uint64(0)>>1) {
|
||||
return 0, false
|
||||
}
|
||||
return int64(typed), true
|
||||
case float64:
|
||||
if typed == float64(int64(typed)) {
|
||||
return int64(typed), true
|
||||
}
|
||||
case float32:
|
||||
if typed == float32(int64(typed)) {
|
||||
return int64(typed), true
|
||||
}
|
||||
case json.Number:
|
||||
parsed, err := typed.Int64()
|
||||
return parsed, err == nil
|
||||
case string:
|
||||
parsed, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
|
||||
return parsed, err == nil
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func anyFloat64(value any) (float64, bool) {
|
||||
switch typed := value.(type) {
|
||||
case int:
|
||||
return float64(typed), true
|
||||
case int64:
|
||||
return float64(typed), true
|
||||
case uint64:
|
||||
return float64(typed), true
|
||||
case float64:
|
||||
return typed, true
|
||||
case float32:
|
||||
return float64(typed), true
|
||||
case json.Number:
|
||||
parsed, err := typed.Float64()
|
||||
return parsed, err == nil
|
||||
case string:
|
||||
parsed, err := strconv.ParseFloat(strings.TrimSpace(typed), 64)
|
||||
return parsed, err == nil
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func scumScalarEqual(left any, right any) bool {
|
||||
if leftInt, ok := anyInt64(left); ok {
|
||||
if rightInt, rightOK := anyInt64(right); rightOK {
|
||||
return leftInt == rightInt
|
||||
}
|
||||
}
|
||||
if leftFloat, ok := anyFloat64(left); ok {
|
||||
if rightFloat, rightOK := anyFloat64(right); rightOK {
|
||||
return leftFloat == rightFloat
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprint(left)) == strings.TrimSpace(fmt.Sprint(right))
|
||||
}
|
||||
|
||||
func scumScalarString(value any) string {
|
||||
if parsed, ok := anyInt64(value); ok {
|
||||
return strconv.FormatInt(parsed, 10)
|
||||
}
|
||||
if parsed, ok := anyFloat64(value); ok {
|
||||
return strconv.FormatFloat(parsed, 'f', -1, 64)
|
||||
}
|
||||
if typed, ok := value.(bool); ok {
|
||||
return strconv.FormatBool(typed)
|
||||
}
|
||||
return bounded(strings.TrimSpace(fmt.Sprint(value)), 512)
|
||||
}
|
||||
|
||||
func operationSafeSummary(templateKey, playerID string, payload map[string]any) domain.SCUMSafeSummary {
|
||||
details := map[string]string{"template": templateKey}
|
||||
if playerID != "" {
|
||||
details["playerId"] = playerID
|
||||
}
|
||||
if amount, ok := operationInteger(payload, "fame", "amount", "balance", "value", "normalBalance", "goldBalance"); ok {
|
||||
details["value"] = fmt.Sprintf("%d", amount)
|
||||
}
|
||||
return domain.SCUMSafeSummary{Title: "Typed SCUM operation", Message: "RCON text is generated server-side and is not stored in the operation record.", Details: details}
|
||||
}
|
||||
|
||||
func scumOperationTemplate(plugin domain.GamePlugin, key string) (domain.GameClientBridgeOperationTemplateDeclaration, bool) {
|
||||
for _, template := range plugin.GameClientBridge.OperationTemplates {
|
||||
if template.Key == key {
|
||||
return template, true
|
||||
}
|
||||
}
|
||||
return domain.GameClientBridgeOperationTemplateDeclaration{}, false
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
func TestSCUMRCONOperationApprovalDispatchesTransientCommandAndConfirms(t *testing.T) {
|
||||
svc, session, runSession, instance := newSourceRCONFixture(t)
|
||||
seedSCUMOperationTemplates(t, svc, instance.PluginID)
|
||||
request := domain.SCUMOperationRequest{TemplateKey: "player.fame.set", PlayerID: "76561198000000001", Payload: map[string]any{"fame": 123}, Reason: "restore fame", IdempotencyKey: "fame-restore-1"}
|
||||
operation, err := svc.RequestSCUMOperationForSession(session, instance.ID, request)
|
||||
if err != nil || operation.Status != domain.SCUMWorkflowStepWaiting {
|
||||
t.Fatalf("request operation=%+v err=%v", operation, err)
|
||||
}
|
||||
duplicate, err := svc.RequestSCUMOperationForSession(session, instance.ID, request)
|
||||
if err != nil || duplicate.ID != operation.ID {
|
||||
t.Fatalf("duplicate should return original operation: duplicate=%+v err=%v", duplicate, err)
|
||||
}
|
||||
approved, err := svc.ApproveSCUMOperationForSession(session, operation.ID)
|
||||
if err != nil || approved.Status != domain.SCUMWorkflowStepQueued || approved.RunJobID == "" {
|
||||
t.Fatalf("approve operation=%+v err=%v", approved, err)
|
||||
}
|
||||
job, err := svc.store.Jobs().Get(approved.RunJobID)
|
||||
if err != nil {
|
||||
t.Fatalf("get operation job: %v", err)
|
||||
}
|
||||
serializedOperation, _ := json.Marshal(approved)
|
||||
serializedJob, _ := json.Marshal(job)
|
||||
for _, forbidden := range []string{"#SetFamePoints", "SetCurrencyBalance", "password="} {
|
||||
if strings.Contains(string(serializedOperation), forbidden) || strings.Contains(string(serializedJob), forbidden) {
|
||||
t.Fatalf("operation/job persisted raw RCON text %q: operation=%s job=%s", forbidden, serializedOperation, serializedJob)
|
||||
}
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: "run-local", SessionToken: runSession, Capabilities: []string{domain.JobCapabilityRemoteRunRCONCommand}, Capacity: domain.RunCapacity{MaxJobs: 1}})
|
||||
if err != nil || !claim.HasJob || claim.Job == nil || claim.Job.JobID != approved.RunJobID {
|
||||
t.Fatalf("claim operation RCON job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
ack, err := svc.AckRunJob(domain.RunJobAck{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Message: "accepted"})
|
||||
if err != nil || !ack.Accepted {
|
||||
t.Fatalf("ack operation RCON job: ack=%+v err=%v", ack, err)
|
||||
}
|
||||
input, err := svc.GetSourceRCONExecutionInput(domain.SourceRCONExecutionInputRequest{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: ack.Job.LeaseToken, Attempt: ack.Job.Attempt})
|
||||
if err != nil {
|
||||
t.Fatalf("read transient operation command: %v", err)
|
||||
}
|
||||
if input.Command != "#SetFamePoints 123 \"76561198000000001\"" {
|
||||
t.Fatalf("unexpected generated RCON command: %q", input.Command)
|
||||
}
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: ack.Job.LeaseToken, Attempt: ack.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100}, ExecutionResult: domain.JobExecutionResult{Kind: "source-rcon.succeeded", AuditSummary: "typed RCON delivered"}}); err != nil {
|
||||
t.Fatalf("complete operation job: %v", err)
|
||||
}
|
||||
reconciled, err := svc.ReconcileSCUMOperation(approved.ID)
|
||||
if err != nil || reconciled.Status != domain.SCUMWorkflowStepConfirming {
|
||||
t.Fatalf("expected confirming after delivery before readback: %+v err=%v", reconciled, err)
|
||||
}
|
||||
confirmed, err := svc.ConfirmSCUMOperation(approved.ID, domain.SCUMOperationConfirmation{Status: "confirmed", ConfirmedFields: map[string]any{"fame": 123}, ObservedAt: fixedTime.Add(time.Minute)})
|
||||
if err != nil || confirmed.Status != domain.SCUMWorkflowStepConfirmed || confirmed.CompletedAt.IsZero() {
|
||||
t.Fatalf("confirm operation=%+v err=%v", confirmed, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMRCONOperationPermissionUnknownAndConfirmationFailure(t *testing.T) {
|
||||
svc, session, runSession, instance := newSourceRCONFixture(t)
|
||||
seedSCUMOperationTemplates(t, svc, instance.PluginID)
|
||||
adminOnly, err := svc.RequestSCUMOperationForSession(session, instance.ID, domain.SCUMOperationRequest{TemplateKey: "player.currency.gold.set", PlayerID: "76561198000000002", Payload: map[string]any{"amount": 9}, Reason: "admin-only", IdempotencyKey: "gold-admin-only"})
|
||||
if err != nil {
|
||||
t.Fatalf("request admin-only operation: %v", err)
|
||||
}
|
||||
if _, err := svc.ApproveSCUMOperationForSession(session, adminOnly.ID); err != ErrForbidden {
|
||||
t.Fatalf("expected platform-admin approval denial, got %v", err)
|
||||
}
|
||||
operation, err := svc.RequestSCUMOperationForSession(session, instance.ID, domain.SCUMOperationRequest{TemplateKey: "player.currency.normal.set", PlayerID: "76561198000000002", Payload: map[string]any{"amount": 500}, Reason: "repair balance", IdempotencyKey: "normal-unknown"})
|
||||
if err != nil {
|
||||
t.Fatalf("request normal currency operation: %v", err)
|
||||
}
|
||||
approved, err := svc.ApproveSCUMOperationForSession(session, operation.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("approve normal currency operation: %v", err)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: "run-local", SessionToken: runSession, Capabilities: []string{domain.JobCapabilityRemoteRunRCONCommand}, Capacity: domain.RunCapacity{MaxJobs: 1}})
|
||||
if err != nil || !claim.HasJob || claim.Job == nil || claim.Job.JobID != approved.RunJobID {
|
||||
t.Fatalf("claim normal currency job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
ack, err := svc.AckRunJob(domain.RunJobAck{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt})
|
||||
if err != nil {
|
||||
t.Fatalf("ack normal currency job: %v", err)
|
||||
}
|
||||
if _, err := svc.GetSourceRCONExecutionInput(domain.SourceRCONExecutionInputRequest{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: ack.Job.LeaseToken, Attempt: ack.Job.Attempt}); err != nil {
|
||||
t.Fatalf("consume normal currency command: %v", err)
|
||||
}
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: ack.Job.LeaseToken, Attempt: ack.Job.Attempt, State: domain.JobStateFailed, Progress: domain.RunJobProgressReport{Percent: 100}, ExecutionResult: domain.JobExecutionResult{Kind: "source-rcon.unknown", AuditSummary: "unknown command state"}}); err != nil {
|
||||
t.Fatalf("complete unknown operation job: %v", err)
|
||||
}
|
||||
unknown, err := svc.ReconcileSCUMOperation(approved.ID)
|
||||
if err != nil || unknown.Status != domain.SCUMWorkflowStepUnknown {
|
||||
t.Fatalf("expected unknown terminal state: %+v err=%v", unknown, err)
|
||||
}
|
||||
failure, err := svc.ConfirmSCUMOperation(operation.ID, domain.SCUMOperationConfirmation{Status: "failed", SafeSummary: domain.SCUMSafeSummary{Title: "Readback mismatch", Message: "Projection did not match expected currency."}, ObservedAt: time.Date(2026, 8, 10, 12, 0, 0, 0, time.UTC)})
|
||||
if err != nil || failure.Status != domain.SCUMWorkflowStepFailed {
|
||||
t.Fatalf("expected confirmation failure: %+v err=%v", failure, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMSQLiteMutationOperationSafetyGatesAndDispatchesTypedJob(t *testing.T) {
|
||||
svc, session, runSession, instance := newSourceRCONFixture(t)
|
||||
seedSCUMOperationTemplates(t, svc, instance.PluginID)
|
||||
adminSession := enableSCUMSQLiteMutationOperationSupport(t, svc, instance)
|
||||
if _, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: instance.ID, PluginID: instance.PluginID, Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 1, Checksum: "sha256:profile-online", ObservedAt: fixedTime, Rows: []map[string]any{{"gamePlayerId": "76561198000000855", "displayName": "Attribute Tester", "online": true, "855": 100}}}); err != nil {
|
||||
t.Fatalf("seed online projection: %v", err)
|
||||
}
|
||||
operation, err := svc.RequestSCUMOperationForSession(session, instance.ID, domain.SCUMOperationRequest{TemplateKey: "player.attribute.855.set", PlayerID: "76561198000000855", Payload: map[string]any{"fieldKey": "855", "before": 100, "after": 150, "safetyWindow": "maintenance-2026-08-10", "backupRef": "snapshot://scum/server-rcon/20260810"}, Reason: "repair attribute 855", IdempotencyKey: "attribute-855-1"})
|
||||
if err != nil || operation.Status != domain.SCUMWorkflowStepWaiting {
|
||||
t.Fatalf("request sqlite mutation=%+v err=%v", operation, err)
|
||||
}
|
||||
waiting, err := svc.ApproveSCUMOperationForSession(adminSession, operation.ID)
|
||||
if err != nil || waiting.Status != domain.SCUMWorkflowStepWaiting || waiting.RunJobID != "" || !strings.Contains(waiting.SafeSummary.Title, "离线") {
|
||||
t.Fatalf("online player should block dispatch: %+v err=%v", waiting, err)
|
||||
}
|
||||
if _, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: instance.ID, PluginID: instance.PluginID, Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 2, Checksum: "sha256:profile-offline", ObservedAt: fixedTime.Add(time.Minute), Rows: []map[string]any{{"gamePlayerId": "76561198000000855", "displayName": "Attribute Tester", "online": false, "855": 100}}}); err != nil {
|
||||
t.Fatalf("seed offline projection: %v", err)
|
||||
}
|
||||
approved, err := svc.ApproveSCUMOperationForSession(adminSession, operation.ID)
|
||||
if err != nil || approved.Status != domain.SCUMWorkflowStepQueued || approved.RunJobID == "" {
|
||||
t.Fatalf("approve sqlite mutation=%+v err=%v", approved, err)
|
||||
}
|
||||
job, err := svc.store.Jobs().Get(approved.RunJobID)
|
||||
if err != nil {
|
||||
t.Fatalf("get sqlite mutation job: %v", err)
|
||||
}
|
||||
if job.Capability != domain.JobCapabilityRemoteRunProtectedSQL || job.ExecutionInput.Inputs["fieldKey"] != "855" || job.ExecutionInput.Inputs["before"] != "100" || job.ExecutionInput.Inputs["after"] != "150" || job.ExecutionInput.Inputs["maxRowsAffected"] != "1" {
|
||||
t.Fatalf("unexpected typed mutation job: %+v", job)
|
||||
}
|
||||
serializedOperation, _ := json.Marshal(approved)
|
||||
serializedJob, _ := json.Marshal(job)
|
||||
for _, forbidden := range []string{"UPDATE ", "DELETE ", "INSERT ", "SELECT ", "SCUM.db", "/Saved/", "requestText"} {
|
||||
if strings.Contains(strings.ToUpper(string(serializedOperation)), strings.ToUpper(forbidden)) || strings.Contains(strings.ToUpper(string(serializedJob)), strings.ToUpper(forbidden)) {
|
||||
t.Fatalf("operation/job persisted raw DB material %q: operation=%s job=%s", forbidden, serializedOperation, serializedJob)
|
||||
}
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: "run-local", SessionToken: runSession, Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}, Capacity: domain.RunCapacity{MaxJobs: 1}})
|
||||
if err != nil || !claim.HasJob || claim.Job == nil || claim.Job.JobID != approved.RunJobID {
|
||||
t.Fatalf("claim sqlite mutation job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
ack, err := svc.AckRunJob(domain.RunJobAck{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Message: "accepted"})
|
||||
if err != nil || !ack.Accepted {
|
||||
t.Fatalf("ack sqlite mutation job: ack=%+v err=%v", ack, err)
|
||||
}
|
||||
mutationChecksum := "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
content := mustJSON(t, map[string]any{"outcome": "succeeded", "affectedRows": 1, "mutationChecksum": mutationChecksum, "confirmationRows": []map[string]any{{"playerId": "76561198000000855", "fieldKey": "855", "value": 150}}})
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: ack.Job.LeaseToken, Attempt: ack.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100}, ExecutionResult: domain.JobExecutionResult{Kind: "scum.sqlite-mutation.succeeded", Checksum: mutationChecksum, AuditSummary: "typed SCUM DB mutation result", Content: content}}); err != nil {
|
||||
t.Fatalf("complete sqlite mutation job: %v", err)
|
||||
}
|
||||
confirmed, err := svc.ReconcileSCUMOperation(approved.ID)
|
||||
if err != nil || confirmed.Status != domain.SCUMWorkflowStepConfirmed || confirmed.Confirmation.AffectedRows != 1 || confirmed.Confirmation.MutationChecksum != mutationChecksum {
|
||||
t.Fatalf("expected confirmed sqlite mutation: %+v err=%v", confirmed, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMSQLiteMutationBlocksMissingSafetyAndStaleBefore(t *testing.T) {
|
||||
svc, session, _, instance := newSourceRCONFixture(t)
|
||||
seedSCUMOperationTemplates(t, svc, instance.PluginID)
|
||||
adminSession := enableSCUMSQLiteMutationOperationSupport(t, svc, instance)
|
||||
if _, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: instance.ID, PluginID: instance.PluginID, Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 1, Checksum: "sha256:profile-855", ObservedAt: fixedTime, Rows: []map[string]any{{"gamePlayerId": "steam-855", "displayName": "Guarded", "online": false, "855": 100}}}); err != nil {
|
||||
t.Fatalf("seed projection: %v", err)
|
||||
}
|
||||
missingSafety, err := svc.RequestSCUMOperationForSession(session, instance.ID, domain.SCUMOperationRequest{TemplateKey: "player.attribute.855.set", PlayerID: "steam-855", Payload: map[string]any{"fieldKey": "855", "before": 100, "after": 101}, Reason: "missing maintenance", IdempotencyKey: "attribute-855-missing-safety"})
|
||||
if err != nil {
|
||||
t.Fatalf("request missing safety mutation: %v", err)
|
||||
}
|
||||
waiting, err := svc.ApproveSCUMOperationForSession(adminSession, missingSafety.ID)
|
||||
if err != nil || waiting.Status != domain.SCUMWorkflowStepWaiting || waiting.RunJobID != "" || !strings.Contains(waiting.SafeSummary.Title, "维护") {
|
||||
t.Fatalf("expected missing maintenance/backup wait: %+v err=%v", waiting, err)
|
||||
}
|
||||
stale, err := svc.RequestSCUMOperationForSession(session, instance.ID, domain.SCUMOperationRequest{TemplateKey: "player.attribute.855.set", PlayerID: "steam-855", Payload: map[string]any{"fieldKey": "855", "before": 99, "after": 101, "safetyWindow": "maintenance-2026-08-10", "backupRef": "snapshot://scum/server-rcon/stale"}, Reason: "stale before", IdempotencyKey: "attribute-855-stale-before"})
|
||||
if err != nil {
|
||||
t.Fatalf("request stale mutation: %v", err)
|
||||
}
|
||||
blocked, err := svc.ApproveSCUMOperationForSession(adminSession, stale.ID)
|
||||
if err != nil || blocked.Status != domain.SCUMWorkflowStepBlocked || blocked.RunJobID != "" || !strings.Contains(blocked.SafeSummary.Title, "before") {
|
||||
t.Fatalf("expected stale before block: %+v err=%v", blocked, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMSQLiteMutationResultValidationRejectsOverBoundRows(t *testing.T) {
|
||||
svc, session, runSession, instance := newSourceRCONFixture(t)
|
||||
seedSCUMOperationTemplates(t, svc, instance.PluginID)
|
||||
adminSession := enableSCUMSQLiteMutationOperationSupport(t, svc, instance)
|
||||
if _, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: instance.ID, PluginID: instance.PluginID, Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 1, Checksum: "sha256:profile-overbound", ObservedAt: fixedTime, Rows: []map[string]any{{"gamePlayerId": "steam-overbound", "online": false, "855": 10}}}); err != nil {
|
||||
t.Fatalf("seed projection: %v", err)
|
||||
}
|
||||
operation, err := svc.RequestSCUMOperationForSession(session, instance.ID, domain.SCUMOperationRequest{TemplateKey: "player.attribute.855.set", PlayerID: "steam-overbound", Payload: map[string]any{"fieldKey": "855", "before": 10, "after": 11, "safetyWindow": "maintenance-2026-08-10", "backupRef": "snapshot://scum/server-rcon/overbound"}, Reason: "overbound test", IdempotencyKey: "attribute-855-overbound"})
|
||||
if err != nil {
|
||||
t.Fatalf("request overbound mutation: %v", err)
|
||||
}
|
||||
approved, err := svc.ApproveSCUMOperationForSession(adminSession, operation.ID)
|
||||
if err != nil || approved.RunJobID == "" {
|
||||
t.Fatalf("approve overbound mutation=%+v err=%v", approved, err)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: "run-local", SessionToken: runSession, Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}, Capacity: domain.RunCapacity{MaxJobs: 1}})
|
||||
if err != nil || !claim.HasJob || claim.Job == nil {
|
||||
t.Fatalf("claim overbound job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
ack, err := svc.AckRunJob(domain.RunJobAck{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt})
|
||||
if err != nil {
|
||||
t.Fatalf("ack overbound job: %v", err)
|
||||
}
|
||||
content := mustJSON(t, map[string]any{"outcome": "succeeded", "affectedRows": 2, "mutationChecksum": "sha256:mutation-overbound"})
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: ack.Job.LeaseToken, Attempt: ack.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100}, ExecutionResult: domain.JobExecutionResult{Kind: "scum.sqlite-mutation.succeeded", Content: content, AuditSummary: "typed SCUM DB mutation result"}}); err != nil {
|
||||
t.Fatalf("complete overbound job: %v", err)
|
||||
}
|
||||
unknown, err := svc.ReconcileSCUMOperation(approved.ID)
|
||||
if err != nil || unknown.Status != domain.SCUMWorkflowStepUnknown {
|
||||
t.Fatalf("expected over-bound rows to become unknown: %+v err=%v", unknown, err)
|
||||
}
|
||||
}
|
||||
|
||||
func seedSCUMOperationTemplates(t *testing.T, svc *CoreService, pluginID string) {
|
||||
t.Helper()
|
||||
plugin, err := svc.store.GamePlugins().Get(pluginID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.game-client.command")
|
||||
plugin.GameClientBridge.OperationTemplates = []domain.GameClientBridgeOperationTemplateDeclaration{
|
||||
{Key: "player.fame.set", Title: "Set player fame", Permission: "server.game-client.command", ApprovalLevel: domain.GameClientBridgeApprovalLevelOperator, Kind: domain.GameClientBridgeOperationKindRCON, TransportKey: "rcon", TargetKey: "rcon", PayloadSchemaRef: "schemas/bridge/player-fame-set.payload.schema.json", TimeoutSeconds: 60, MaxPayloadBytes: 2048, Safety: domain.GameClientBridgeOperationSafety{RequiresApproval: true, RequiresConfirmation: true}},
|
||||
{Key: "player.currency.normal.set", Title: "Set player normal currency", Permission: "server.game-client.command", ApprovalLevel: domain.GameClientBridgeApprovalLevelOperator, Kind: domain.GameClientBridgeOperationKindRCON, TransportKey: "rcon", TargetKey: "rcon", PayloadSchemaRef: "schemas/bridge/player-currency-set.payload.schema.json", TimeoutSeconds: 60, MaxPayloadBytes: 2048, Safety: domain.GameClientBridgeOperationSafety{RequiresApproval: true, RequiresConfirmation: true}},
|
||||
{Key: "player.currency.gold.set", Title: "Set player gold currency", Permission: "server.game-client.command", ApprovalLevel: domain.GameClientBridgeApprovalLevelPlatformAdmin, Kind: domain.GameClientBridgeOperationKindRCON, TransportKey: "rcon", TargetKey: "rcon", PayloadSchemaRef: "schemas/bridge/player-currency-set.payload.schema.json", TimeoutSeconds: 60, MaxPayloadBytes: 2048, Safety: domain.GameClientBridgeOperationSafety{RequiresApproval: true, RequiresConfirmation: true}},
|
||||
}
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatalf("update plugin operation templates: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func enableSCUMSQLiteMutationOperationSupport(t *testing.T, svc *CoreService, instance domain.ServerInstance) string {
|
||||
t.Helper()
|
||||
adminSession := createServiceUserAndLogin(t, svc, domain.User{ID: "platform-admin-scum", DisplayName: "SCUM Admin", Email: "scum-admin@example.test", Roles: []string{"platform-admin"}, PasswordHash: "secret-password"})
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.game-client.maintenance")
|
||||
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, domain.JobCapabilityRemoteRunProtectedSQL)
|
||||
plugin.RemoteAccess.RunCapabilities = append(plugin.RemoteAccess.RunCapabilities, domain.JobCapabilityRemoteRunProtectedSQL)
|
||||
plugin.RemoteAccess.DatabaseEngines = append(plugin.RemoteAccess.DatabaseEngines, "sqlite")
|
||||
plugin.RuntimeProfiles.TransportProfiles = append(plugin.RuntimeProfiles.TransportProfiles, domain.RuntimeTransportProfile{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}})
|
||||
plugin.GameClientBridge.OperationTemplates = append(plugin.GameClientBridge.OperationTemplates, domain.GameClientBridgeOperationTemplateDeclaration{Key: "player.attribute.855.set", Title: "Set player attribute 855", Permission: "server.game-client.maintenance", ApprovalLevel: domain.GameClientBridgeApprovalLevelPlatformAdmin, Kind: domain.GameClientBridgeOperationKindSQLiteMutation, TransportKey: "scum-database", TargetKey: "scum-database", PayloadSchemaRef: "schemas/bridge/player-attribute-855-set.payload.schema.json", ResultSchemaRef: "schemas/bridge/player-attribute-855-set.result.schema.json", ConfirmationSchemaRef: "schemas/bridge/player-attribute-855-set.confirmation.schema.json", TimeoutSeconds: 120, MaxPayloadBytes: 4096, MaxRowsAffected: 1, Mutation: domain.GameClientBridgeOperationMutationDeclaration{FieldKey: "855", TableKey: "prisoner", IdentityKey: "user_profile_id", ValueKey: "value", ConfirmationQueryKey: "scum.player.profile", AllowedValueType: "integer", MinValue: 0, MaxValue: 100000}, Safety: domain.GameClientBridgeOperationSafety{RequiresApproval: true, RequiresOfflinePlayer: true, RequiresMaintenanceWindow: true, RequiresBeforeValue: true, RequiresConfirmation: true, BackupRequired: true}})
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatalf("update SCUM DB mutation plugin: %v", err)
|
||||
}
|
||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunProtectedSQL)
|
||||
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
|
||||
t.Fatalf("update SCUM DB mutation endpoint: %v", err)
|
||||
}
|
||||
return adminSession
|
||||
}
|
||||
|
||||
func mustJSON(t *testing.T, value any) string {
|
||||
t.Helper()
|
||||
encoded, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal test JSON: %v", err)
|
||||
}
|
||||
return string(encoded)
|
||||
}
|
||||
@@ -0,0 +1,803 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/repo"
|
||||
"browser.local/platform/validator"
|
||||
)
|
||||
|
||||
func (svc *CoreService) ApplySCUMObservationResult(result domain.SCUMObservationResult) (domain.SCUMDataObservation, error) {
|
||||
result = domain.CopySCUMObservationResult(result)
|
||||
if strings.TrimSpace(result.ServerInstanceID) == "" {
|
||||
return domain.SCUMDataObservation{}, validationError("serverInstanceId is required")
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(result.ServerInstanceID)
|
||||
if err != nil {
|
||||
return domain.SCUMDataObservation{}, err
|
||||
}
|
||||
if strings.TrimSpace(result.PluginID) == "" {
|
||||
result.PluginID = instance.PluginID
|
||||
}
|
||||
if result.PluginID != instance.PluginID {
|
||||
return domain.SCUMDataObservation{}, validationError("pluginId must match server instance")
|
||||
}
|
||||
if strings.TrimSpace(result.QueryKey) == "" {
|
||||
return domain.SCUMDataObservation{}, validationError("queryKey is required")
|
||||
}
|
||||
if result.ReceivedAt.IsZero() {
|
||||
result.ReceivedAt = svc.now()
|
||||
}
|
||||
if result.ObservedAt.IsZero() {
|
||||
result.ObservedAt = result.ReceivedAt
|
||||
}
|
||||
if result.Status == "" {
|
||||
result.Status = domain.SCUMObservationAccepted
|
||||
}
|
||||
latest, err := svc.latestSCUMObservation(result.ServerInstanceID, result.PluginID, result.QueryKey)
|
||||
if err != nil {
|
||||
return domain.SCUMDataObservation{}, err
|
||||
}
|
||||
if result.Status == domain.SCUMObservationAccepted && !latest.ObservedAt.IsZero() && scumObservationOlder(result, latest) {
|
||||
result.Status = domain.SCUMObservationStale
|
||||
result.ErrorCode = "older_observation"
|
||||
result.SafeSummary = domain.SCUMSafeSummary{Title: "旧观察已忽略", Message: "Run 返回的 SCUM.db 观察早于当前本地投影,未覆盖 last-known-good 数据。"}
|
||||
}
|
||||
observation := domain.SCUMDataObservation{ID: scumObservationID(result), ServerInstanceID: result.ServerInstanceID, PluginID: result.PluginID, Source: result.Source, QueryKey: result.QueryKey, Sequence: result.Sequence, Checksum: result.Checksum, Status: result.Status, ErrorCode: result.ErrorCode, SafeSummary: result.SafeSummary, ObservedAt: result.ObservedAt, ReceivedAt: result.ReceivedAt}
|
||||
if err := svc.upsertSCUMObservation(observation); err != nil {
|
||||
return domain.SCUMDataObservation{}, err
|
||||
}
|
||||
if result.Status != domain.SCUMObservationAccepted {
|
||||
if result.Status == domain.SCUMObservationFailed {
|
||||
return observation, svc.markSCUMQueryStale(result, "observation_failed")
|
||||
}
|
||||
return observation, nil
|
||||
}
|
||||
freshness := domain.SCUMProjectionFreshnessState{Status: domain.SCUMProjectionFresh, ObservationID: observation.ID, Source: observation.Source, QueryKey: observation.QueryKey, Sequence: observation.Sequence, Checksum: observation.Checksum, ObservedAt: observation.ObservedAt, ReceivedAt: observation.ReceivedAt}
|
||||
if err := svc.applySCUMRows(result.QueryKey, result.ServerInstanceID, result.Rows, freshness); err != nil {
|
||||
return domain.SCUMDataObservation{}, err
|
||||
}
|
||||
return observation, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMPlayerLiveStatesForSession(sessionID string, filter domain.SCUMProjectionFilter) ([]domain.SCUMPlayerLiveState, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := svc.store.SCUMPlayerLiveStates().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitSCUMProjectionSlice(&values, filter.Limit)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMSquadsForSession(sessionID string, filter domain.SCUMProjectionFilter) ([]domain.SCUMSquad, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := svc.store.SCUMSquads().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitSCUMProjectionSlice(&values, filter.Limit)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMSquadMembersForSession(sessionID string, filter domain.SCUMProjectionFilter) ([]domain.SCUMSquadMember, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := svc.store.SCUMSquadMembers().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitSCUMProjectionSlice(&values, filter.Limit)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMVehiclesForSession(sessionID string, filter domain.SCUMProjectionFilter) ([]domain.SCUMVehicle, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := svc.store.SCUMVehicles().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitSCUMProjectionSlice(&values, filter.Limit)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMFlagsForSession(sessionID string, filter domain.SCUMProjectionFilter) ([]domain.SCUMFlag, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := svc.store.SCUMFlags().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitSCUMProjectionSlice(&values, filter.Limit)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMCurrentPositionsForSession(sessionID string, filter domain.SCUMProjectionFilter) ([]domain.SCUMCurrentPosition, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := svc.store.SCUMCurrentPositions().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitSCUMProjectionSlice(&values, filter.Limit)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) latestSCUMObservation(serverID, pluginID, queryKey string) (domain.SCUMDataObservation, error) {
|
||||
observations, err := svc.store.SCUMDataObservations().List(domain.SCUMProjectionFilter{ServerInstanceID: serverID, QueryKey: queryKey})
|
||||
if err != nil {
|
||||
return domain.SCUMDataObservation{}, err
|
||||
}
|
||||
var latest domain.SCUMDataObservation
|
||||
for _, observation := range observations {
|
||||
if pluginID != "" && observation.PluginID != pluginID {
|
||||
continue
|
||||
}
|
||||
if latest.ObservedAt.IsZero() || observation.Sequence > latest.Sequence || (observation.Sequence == latest.Sequence && observation.ObservedAt.After(latest.ObservedAt)) {
|
||||
latest = observation
|
||||
}
|
||||
}
|
||||
return latest, nil
|
||||
}
|
||||
|
||||
func scumObservationOlder(next domain.SCUMObservationResult, latest domain.SCUMDataObservation) bool {
|
||||
if next.Sequence > 0 && latest.Sequence > 0 && next.Sequence <= latest.Sequence {
|
||||
return true
|
||||
}
|
||||
return !next.ObservedAt.IsZero() && !latest.ObservedAt.IsZero() && next.ObservedAt.Before(latest.ObservedAt)
|
||||
}
|
||||
|
||||
func (svc *CoreService) upsertSCUMObservation(observation domain.SCUMDataObservation) error {
|
||||
if existing, err := svc.store.SCUMDataObservations().Get(observation.ID); err == nil {
|
||||
existing.Status = observation.Status
|
||||
existing.ErrorCode = observation.ErrorCode
|
||||
existing.SafeSummary = observation.SafeSummary
|
||||
existing.ReceivedAt = observation.ReceivedAt
|
||||
return svc.store.SCUMDataObservations().Update(existing)
|
||||
} else if err != repo.ErrNotFound {
|
||||
return err
|
||||
}
|
||||
return svc.store.SCUMDataObservations().Create(observation)
|
||||
}
|
||||
|
||||
func (svc *CoreService) applySCUMRows(queryKey, serverID string, rows []map[string]any, freshness domain.SCUMProjectionFreshnessState) error {
|
||||
lower := strings.ToLower(queryKey)
|
||||
if strings.Contains(lower, "player") || strings.Contains(lower, "profile") || strings.Contains(lower, "economy") {
|
||||
for _, row := range rows {
|
||||
if err := svc.applySCUMPlayerRow(serverID, row, freshness); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(lower, "squad-member") || strings.Contains(lower, "squad.member") || strings.Contains(lower, "member") {
|
||||
for _, row := range rows {
|
||||
if err := svc.applySCUMSquadMemberRow(serverID, row, freshness); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(lower, "squad") {
|
||||
for _, row := range rows {
|
||||
if err := svc.applySCUMSquadRow(serverID, row, freshness); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(lower, "vehicle") {
|
||||
for _, row := range rows {
|
||||
if err := svc.applySCUMVehicleRow(serverID, row, freshness); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(lower, "flag") {
|
||||
for _, row := range rows {
|
||||
if err := svc.applySCUMFlagRow(serverID, row, freshness); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(lower, "position") || strings.Contains(lower, "coordinate") {
|
||||
for _, row := range rows {
|
||||
if err := svc.applySCUMPositionRow(serverID, row, freshness); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) applySCUMPlayerRow(serverID string, row map[string]any, freshness domain.SCUMProjectionFreshnessState) error {
|
||||
gamePlayerID := firstString(row, "gamePlayerId", "playerId", "steamId", "steam_id")
|
||||
profileID := firstString(row, "userProfileId", "user_profile_id", "profileId")
|
||||
steamID := firstString(row, "steamId", "steam_id")
|
||||
name := firstString(row, "displayName", "name", "playerName")
|
||||
if gamePlayerID == "" && steamID != "" {
|
||||
gamePlayerID = steamID
|
||||
}
|
||||
if gamePlayerID == "" && profileID == "" {
|
||||
return nil
|
||||
}
|
||||
playerRecordID := ""
|
||||
if gamePlayerID != "" {
|
||||
playerRecordID = gamePlayerRecordID(serverID, gamePlayerID)
|
||||
if err := svc.upsertSCUMGamePlayer(serverID, playerRecordID, gamePlayerID, name, freshness.ObservedAt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
idSource := gamePlayerID
|
||||
if idSource == "" {
|
||||
idSource = "profile-" + profileID
|
||||
}
|
||||
id := scumProjectionID("player-live", serverID, idSource)
|
||||
state, err := svc.store.SCUMPlayerLiveStates().Get(id)
|
||||
if err == repo.ErrNotFound {
|
||||
state = domain.SCUMPlayerLiveState{ID: id, ServerInstanceID: serverID, GamePlayerRecordID: playerRecordID, GamePlayerID: gamePlayerID, UserProfileID: profileID, SteamID: steamID, DisplayName: name, Freshness: domain.SCUMProjectionStateUnknown(), CreatedAt: svc.now()}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if isProjectionOlder(freshness, state.Freshness) {
|
||||
return nil
|
||||
}
|
||||
state.GamePlayerRecordID = coalesceString(playerRecordID, state.GamePlayerRecordID)
|
||||
state.GamePlayerID = coalesceString(gamePlayerID, state.GamePlayerID)
|
||||
state.UserProfileID = coalesceString(profileID, state.UserProfileID)
|
||||
state.SteamID = coalesceString(steamID, state.SteamID)
|
||||
state.DisplayName = coalesceString(name, state.DisplayName)
|
||||
state.SquadID = coalesceString(firstString(row, "squadId", "squad_id"), state.SquadID)
|
||||
state.SquadName = coalesceString(firstString(row, "squadName", "squad_name"), state.SquadName)
|
||||
if value, ok := firstFloat(row, "famePoints", "fame_points", "fame"); ok {
|
||||
state.FamePoints = value
|
||||
}
|
||||
if value, ok := firstFloat(row, "normalBalance", "currencyNormal", "money", "normal_balance"); ok {
|
||||
state.NormalBalance = value
|
||||
}
|
||||
if value, ok := firstFloat(row, "goldBalance", "currencyGold", "gold", "gold_balance"); ok {
|
||||
state.GoldBalance = value
|
||||
}
|
||||
if value, ok := firstBool(row, "online", "isOnline"); ok {
|
||||
state.Online = value
|
||||
}
|
||||
state.LastLoginAt = coalesceTime(firstTime(row, "lastLoginAt", "last_login_at"), state.LastLoginAt)
|
||||
state.LastLogoutAt = coalesceTime(firstTime(row, "lastLogoutAt", "last_logout_at"), state.LastLogoutAt)
|
||||
state.LastSaveTime = coalesceTime(firstTime(row, "lastSaveTime", "last_save_time"), state.LastSaveTime)
|
||||
if position, ok := scumPositionFromRow(serverID, domain.SCUMProjectionSubjectPlayer, gamePlayerID, row, freshness); ok {
|
||||
position.GamePlayerRecordID = playerRecordID
|
||||
position.GamePlayerID = gamePlayerID
|
||||
state.Position = position
|
||||
if err := svc.upsertSCUMPosition(position); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
state.UnknownFields = unknownRowFields(row, "gamePlayerId", "playerId", "steamId", "steam_id", "userProfileId", "user_profile_id", "profileId", "displayName", "name", "playerName", "squadId", "squad_id", "squadName", "squad_name", "famePoints", "fame_points", "fame", "normalBalance", "currencyNormal", "money", "normal_balance", "goldBalance", "currencyGold", "gold", "gold_balance", "online", "isOnline", "lastLoginAt", "last_login_at", "lastLogoutAt", "last_logout_at", "lastSaveTime", "last_save_time", "x", "y", "z", "worldX", "worldY", "worldZ", "mapId", "mapVersion")
|
||||
state.Freshness = freshness
|
||||
state.UpdatedAt = svc.now()
|
||||
if err == repo.ErrNotFound {
|
||||
return svc.store.SCUMPlayerLiveStates().Create(state)
|
||||
}
|
||||
return svc.store.SCUMPlayerLiveStates().Update(state)
|
||||
}
|
||||
|
||||
func (svc *CoreService) applySCUMSquadRow(serverID string, row map[string]any, freshness domain.SCUMProjectionFreshnessState) error {
|
||||
squadID := firstString(row, "squadId", "squad_id", "id")
|
||||
if squadID == "" {
|
||||
return nil
|
||||
}
|
||||
id := scumProjectionID("squad", serverID, squadID)
|
||||
value, err := svc.store.SCUMSquads().Get(id)
|
||||
if err == repo.ErrNotFound {
|
||||
value = domain.SCUMSquad{ID: id, ServerInstanceID: serverID, SquadID: squadID, Freshness: domain.SCUMProjectionStateUnknown(), CreatedAt: svc.now()}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if isProjectionOlder(freshness, value.Freshness) {
|
||||
return nil
|
||||
}
|
||||
value.Name = coalesceString(firstString(row, "name", "squadName", "squad_name"), value.Name)
|
||||
value.LeaderProfileID = coalesceString(firstString(row, "leaderProfileId", "leader_profile_id"), value.LeaderProfileID)
|
||||
value.LeaderPlayerID = coalesceString(firstString(row, "leaderPlayerId", "leader_player_id", "leaderSteamId"), value.LeaderPlayerID)
|
||||
if memberCount, ok := firstInt(row, "memberCount", "member_count"); ok {
|
||||
value.MemberCount = memberCount
|
||||
}
|
||||
if score, ok := firstFloat(row, "score", "fame", "points"); ok {
|
||||
value.Score = score
|
||||
}
|
||||
value.UnknownFields = unknownRowFields(row, "squadId", "squad_id", "id", "name", "squadName", "squad_name", "leaderProfileId", "leader_profile_id", "leaderPlayerId", "leader_player_id", "leaderSteamId", "memberCount", "member_count", "score", "fame", "points")
|
||||
value.Freshness = freshness
|
||||
value.UpdatedAt = svc.now()
|
||||
if err == repo.ErrNotFound {
|
||||
return svc.store.SCUMSquads().Create(value)
|
||||
}
|
||||
return svc.store.SCUMSquads().Update(value)
|
||||
}
|
||||
|
||||
func (svc *CoreService) applySCUMSquadMemberRow(serverID string, row map[string]any, freshness domain.SCUMProjectionFreshnessState) error {
|
||||
squadID := firstString(row, "squadId", "squad_id")
|
||||
profileID := firstString(row, "userProfileId", "user_profile_id", "profileId")
|
||||
gamePlayerID := firstString(row, "gamePlayerId", "playerId", "steamId", "steam_id")
|
||||
if squadID == "" || (profileID == "" && gamePlayerID == "") {
|
||||
return nil
|
||||
}
|
||||
playerRecordID := ""
|
||||
if gamePlayerID != "" {
|
||||
playerRecordID = gamePlayerRecordID(serverID, gamePlayerID)
|
||||
if err := svc.upsertSCUMGamePlayer(serverID, playerRecordID, gamePlayerID, firstString(row, "displayName", "name", "playerName"), freshness.ObservedAt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
id := scumProjectionID("squad-member", serverID, squadID+"/"+coalesceString(profileID, gamePlayerID))
|
||||
value, err := svc.store.SCUMSquadMembers().Get(id)
|
||||
if err == repo.ErrNotFound {
|
||||
value = domain.SCUMSquadMember{ID: id, ServerInstanceID: serverID, SquadID: squadID, UserProfileID: profileID, GamePlayerRecordID: playerRecordID, GamePlayerID: gamePlayerID, Freshness: domain.SCUMProjectionStateUnknown(), CreatedAt: svc.now()}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if isProjectionOlder(freshness, value.Freshness) {
|
||||
return nil
|
||||
}
|
||||
value.UserProfileID = coalesceString(profileID, value.UserProfileID)
|
||||
value.GamePlayerRecordID = coalesceString(playerRecordID, value.GamePlayerRecordID)
|
||||
value.GamePlayerID = coalesceString(gamePlayerID, value.GamePlayerID)
|
||||
value.SteamID = coalesceString(firstString(row, "steamId", "steam_id"), value.SteamID)
|
||||
value.DisplayName = coalesceString(firstString(row, "displayName", "name", "playerName"), value.DisplayName)
|
||||
value.Rank = coalesceString(firstString(row, "rank", "role"), value.Rank)
|
||||
if isLeader, ok := firstBool(row, "isLeader", "leader"); ok {
|
||||
value.IsLeader = isLeader
|
||||
}
|
||||
value.JoinedAt = coalesceTime(firstTime(row, "joinedAt", "joined_at"), value.JoinedAt)
|
||||
value.UnknownFields = unknownRowFields(row, "squadId", "squad_id", "userProfileId", "user_profile_id", "profileId", "gamePlayerId", "playerId", "steamId", "steam_id", "displayName", "name", "playerName", "rank", "role", "isLeader", "leader", "joinedAt", "joined_at")
|
||||
value.Freshness = freshness
|
||||
value.UpdatedAt = svc.now()
|
||||
if err == repo.ErrNotFound {
|
||||
return svc.store.SCUMSquadMembers().Create(value)
|
||||
}
|
||||
return svc.store.SCUMSquadMembers().Update(value)
|
||||
}
|
||||
|
||||
func (svc *CoreService) applySCUMVehicleRow(serverID string, row map[string]any, freshness domain.SCUMProjectionFreshnessState) error {
|
||||
vehicleID := firstString(row, "vehicleId", "vehicle_id", "id")
|
||||
entityID := firstString(row, "entityId", "entity_id")
|
||||
if vehicleID == "" && entityID != "" {
|
||||
vehicleID = entityID
|
||||
}
|
||||
if vehicleID == "" {
|
||||
return nil
|
||||
}
|
||||
id := scumProjectionID("vehicle", serverID, vehicleID)
|
||||
value, err := svc.store.SCUMVehicles().Get(id)
|
||||
if err == repo.ErrNotFound {
|
||||
value = domain.SCUMVehicle{ID: id, ServerInstanceID: serverID, VehicleID: vehicleID, Freshness: domain.SCUMProjectionStateUnknown(), CreatedAt: svc.now()}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if isProjectionOlder(freshness, value.Freshness) {
|
||||
return nil
|
||||
}
|
||||
value.EntityID = coalesceString(entityID, value.EntityID)
|
||||
value.ClassName = coalesceString(firstString(row, "className", "class", "type"), value.ClassName)
|
||||
value.Label = coalesceString(firstString(row, "label", "vehicleName", "name"), value.Label)
|
||||
if value.Label == "" {
|
||||
value.Label = coalesceString(value.ClassName, "Unknown vehicle")
|
||||
}
|
||||
value.OwnerProfileID = coalesceString(firstString(row, "ownerProfileId", "owner_profile_id", "userProfileId", "user_profile_id"), value.OwnerProfileID)
|
||||
value.OwnerPlayerID = coalesceString(firstString(row, "ownerPlayerId", "owner_player_id", "steamId", "steam_id"), value.OwnerPlayerID)
|
||||
value.SquadID = coalesceString(firstString(row, "squadId", "squad_id"), value.SquadID)
|
||||
if position, ok := scumPositionFromRow(serverID, domain.SCUMProjectionSubjectVehicle, vehicleID, row, freshness); ok {
|
||||
position.VehicleID = vehicleID
|
||||
position.EntityID = entityID
|
||||
value.Position = position
|
||||
if err := svc.upsertSCUMPosition(position); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
value.UnknownFields = unknownRowFields(row, "vehicleId", "vehicle_id", "id", "entityId", "entity_id", "className", "class", "type", "label", "vehicleName", "name", "ownerProfileId", "owner_profile_id", "userProfileId", "user_profile_id", "ownerPlayerId", "owner_player_id", "steamId", "steam_id", "squadId", "squad_id", "x", "y", "z", "worldX", "worldY", "worldZ", "mapId", "mapVersion")
|
||||
value.Freshness = freshness
|
||||
value.UpdatedAt = svc.now()
|
||||
if err == repo.ErrNotFound {
|
||||
return svc.store.SCUMVehicles().Create(value)
|
||||
}
|
||||
return svc.store.SCUMVehicles().Update(value)
|
||||
}
|
||||
|
||||
func (svc *CoreService) applySCUMFlagRow(serverID string, row map[string]any, freshness domain.SCUMProjectionFreshnessState) error {
|
||||
flagID := firstString(row, "flagId", "flag_id", "baseElementId", "base_element_id", "id")
|
||||
entityID := firstString(row, "entityId", "entity_id")
|
||||
if flagID == "" && entityID != "" {
|
||||
flagID = entityID
|
||||
}
|
||||
if flagID == "" {
|
||||
return nil
|
||||
}
|
||||
id := scumProjectionID("flag", serverID, flagID)
|
||||
value, err := svc.store.SCUMFlags().Get(id)
|
||||
if err == repo.ErrNotFound {
|
||||
value = domain.SCUMFlag{ID: id, ServerInstanceID: serverID, FlagID: flagID, Freshness: domain.SCUMProjectionStateUnknown(), CreatedAt: svc.now()}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if isProjectionOlder(freshness, value.Freshness) {
|
||||
return nil
|
||||
}
|
||||
value.EntityID = coalesceString(entityID, value.EntityID)
|
||||
value.OwnerProfileID = coalesceString(firstString(row, "ownerProfileId", "owner_profile_id", "userProfileId", "user_profile_id"), value.OwnerProfileID)
|
||||
value.OwnerPlayerID = coalesceString(firstString(row, "ownerPlayerId", "owner_player_id", "steamId", "steam_id"), value.OwnerPlayerID)
|
||||
value.OwnerSquadID = coalesceString(firstString(row, "ownerSquadId", "owner_squad_id", "squadId", "squad_id"), value.OwnerSquadID)
|
||||
value.OwnerSquadName = coalesceString(firstString(row, "ownerSquadName", "owner_squad_name", "squadName", "squad_name"), value.OwnerSquadName)
|
||||
value.OwnershipConfidence = coalesceString(firstString(row, "ownershipConfidence", "ownership_confidence"), value.OwnershipConfidence)
|
||||
if value.OwnershipConfidence == "" {
|
||||
value.OwnershipConfidence = "unknown"
|
||||
}
|
||||
if position, ok := scumPositionFromRow(serverID, domain.SCUMProjectionSubjectFlag, flagID, row, freshness); ok {
|
||||
position.EntityID = entityID
|
||||
value.Position = position
|
||||
if err := svc.upsertSCUMPosition(position); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
value.UnknownFields = unknownRowFields(row, "flagId", "flag_id", "baseElementId", "base_element_id", "id", "entityId", "entity_id", "ownerProfileId", "owner_profile_id", "userProfileId", "user_profile_id", "ownerPlayerId", "owner_player_id", "steamId", "steam_id", "ownerSquadId", "owner_squad_id", "squadId", "squad_id", "ownerSquadName", "owner_squad_name", "squadName", "squad_name", "ownershipConfidence", "ownership_confidence", "x", "y", "z", "worldX", "worldY", "worldZ", "mapId", "mapVersion")
|
||||
value.Freshness = freshness
|
||||
value.UpdatedAt = svc.now()
|
||||
if err == repo.ErrNotFound {
|
||||
return svc.store.SCUMFlags().Create(value)
|
||||
}
|
||||
return svc.store.SCUMFlags().Update(value)
|
||||
}
|
||||
|
||||
func (svc *CoreService) applySCUMPositionRow(serverID string, row map[string]any, freshness domain.SCUMProjectionFreshnessState) error {
|
||||
subjectType := domain.SCUMProjectionSubject(firstString(row, "subjectType", "subject_type"))
|
||||
if subjectType == "" {
|
||||
if firstString(row, "vehicleId", "vehicle_id") != "" {
|
||||
subjectType = domain.SCUMProjectionSubjectVehicle
|
||||
} else {
|
||||
subjectType = domain.SCUMProjectionSubjectPlayer
|
||||
}
|
||||
}
|
||||
subjectID := firstString(row, "subjectId", "subject_id", "gamePlayerId", "playerId", "vehicleId", "flagId", "entityId", "id")
|
||||
position, ok := scumPositionFromRow(serverID, subjectType, subjectID, row, freshness)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
position.GamePlayerID = firstString(row, "gamePlayerId", "playerId", "steamId", "steam_id")
|
||||
if position.GamePlayerID != "" {
|
||||
position.GamePlayerRecordID = gamePlayerRecordID(serverID, position.GamePlayerID)
|
||||
}
|
||||
position.VehicleID = firstString(row, "vehicleId", "vehicle_id")
|
||||
position.EntityID = firstString(row, "entityId", "entity_id")
|
||||
return svc.upsertSCUMPosition(position)
|
||||
}
|
||||
|
||||
func (svc *CoreService) upsertSCUMGamePlayer(serverID, recordID, gamePlayerID, displayName string, observedAt time.Time) error {
|
||||
if gamePlayerID == "" || recordID == "" {
|
||||
return nil
|
||||
}
|
||||
if observedAt.IsZero() {
|
||||
observedAt = svc.now()
|
||||
}
|
||||
player, err := svc.store.GamePlayers().Get(recordID)
|
||||
if err == repo.ErrNotFound {
|
||||
return svc.store.GamePlayers().Create(domain.GamePlayer{ID: recordID, ServerInstanceID: serverID, GamePlayerID: gamePlayerID, DisplayName: displayName, FirstSeenAt: observedAt, LastSeenAt: observedAt, LastEventAt: observedAt, CreatedAt: svc.now(), UpdatedAt: svc.now()})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if observedAt.Before(player.LastEventAt) {
|
||||
return nil
|
||||
}
|
||||
player.DisplayName = coalesceString(displayName, player.DisplayName)
|
||||
player.LastSeenAt = maxTime(player.LastSeenAt, observedAt)
|
||||
player.LastEventAt = observedAt
|
||||
player.UpdatedAt = svc.now()
|
||||
return svc.store.GamePlayers().Update(player)
|
||||
}
|
||||
|
||||
func (svc *CoreService) projectSCUMLoginLiveState(player domain.GamePlayer, batch domain.LogBatchIngest, entry domain.LogEntry, observedAt time.Time, online bool, reason string) error {
|
||||
if player.ID == "" || player.GamePlayerID == "" {
|
||||
return nil
|
||||
}
|
||||
freshness := domain.SCUMProjectionFreshnessState{Status: domain.SCUMProjectionFresh, ObservationID: entryID(batch.LogStreamID, entry.Seq), Source: "login-log", QueryKey: strings.TrimSpace(entry.Fields["eventType"]), Sequence: entry.Seq, Checksum: validator.LogLineChecksum(entry.Line), ObservedAt: observedAt, ReceivedAt: svc.now()}
|
||||
id := scumProjectionID("player-live", player.ServerInstanceID, player.GamePlayerID)
|
||||
state, err := svc.store.SCUMPlayerLiveStates().Get(id)
|
||||
if err == repo.ErrNotFound {
|
||||
state = domain.SCUMPlayerLiveState{ID: id, ServerInstanceID: player.ServerInstanceID, GamePlayerRecordID: player.ID, GamePlayerID: player.GamePlayerID, DisplayName: player.DisplayName, Freshness: domain.SCUMProjectionStateUnknown(), CreatedAt: svc.now()}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if isProjectionOlder(freshness, state.Freshness) {
|
||||
return nil
|
||||
}
|
||||
state.GamePlayerRecordID = player.ID
|
||||
state.GamePlayerID = player.GamePlayerID
|
||||
state.DisplayName = player.DisplayName
|
||||
state.Online = online
|
||||
if online {
|
||||
state.LastLoginAt = observedAt
|
||||
} else {
|
||||
state.LastLogoutAt = observedAt
|
||||
}
|
||||
state.Freshness = freshness
|
||||
if reason != "" {
|
||||
state.UnknownFields = domain.CopyGameClientBridgePayload(map[string]any{"lastLogoutReason": bounded(reason, 80)})
|
||||
}
|
||||
state.UpdatedAt = svc.now()
|
||||
if err == repo.ErrNotFound {
|
||||
return svc.store.SCUMPlayerLiveStates().Create(state)
|
||||
}
|
||||
return svc.store.SCUMPlayerLiveStates().Update(state)
|
||||
}
|
||||
|
||||
func (svc *CoreService) upsertSCUMPosition(position domain.SCUMCurrentPosition) error {
|
||||
existing, err := svc.store.SCUMCurrentPositions().Get(position.ID)
|
||||
if err == repo.ErrNotFound {
|
||||
position.CreatedAt = svc.now()
|
||||
position.UpdatedAt = svc.now()
|
||||
return svc.store.SCUMCurrentPositions().Create(position)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isProjectionOlder(position.Freshness, existing.Freshness) {
|
||||
return nil
|
||||
}
|
||||
position.CreatedAt = existing.CreatedAt
|
||||
position.UpdatedAt = svc.now()
|
||||
return svc.store.SCUMCurrentPositions().Update(position)
|
||||
}
|
||||
|
||||
func (svc *CoreService) markSCUMQueryStale(result domain.SCUMObservationResult, reason string) error {
|
||||
freshness := domain.SCUMProjectionFreshnessState{Status: domain.SCUMProjectionStale, ObservationID: scumObservationID(result), Source: result.Source, QueryKey: result.QueryKey, Sequence: result.Sequence, Checksum: result.Checksum, StaleReason: reason, ObservedAt: result.ObservedAt, ReceivedAt: result.ReceivedAt}
|
||||
lower := strings.ToLower(result.QueryKey)
|
||||
if strings.Contains(lower, "player") || strings.Contains(lower, "profile") || strings.Contains(lower, "economy") {
|
||||
values, err := svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: result.ServerInstanceID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, value := range values {
|
||||
if !isProjectionOlder(freshness, value.Freshness) {
|
||||
value.Freshness = freshness
|
||||
value.UpdatedAt = svc.now()
|
||||
if err := svc.store.SCUMPlayerLiveStates().Update(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(lower, "squad") {
|
||||
values, err := svc.store.SCUMSquads().List(domain.SCUMProjectionFilter{ServerInstanceID: result.ServerInstanceID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, value := range values {
|
||||
if !isProjectionOlder(freshness, value.Freshness) {
|
||||
value.Freshness = freshness
|
||||
value.UpdatedAt = svc.now()
|
||||
if err := svc.store.SCUMSquads().Update(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(lower, "vehicle") {
|
||||
values, err := svc.store.SCUMVehicles().List(domain.SCUMProjectionFilter{ServerInstanceID: result.ServerInstanceID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, value := range values {
|
||||
if !isProjectionOlder(freshness, value.Freshness) {
|
||||
value.Freshness = freshness
|
||||
value.UpdatedAt = svc.now()
|
||||
if err := svc.store.SCUMVehicles().Update(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(lower, "flag") {
|
||||
values, err := svc.store.SCUMFlags().List(domain.SCUMProjectionFilter{ServerInstanceID: result.ServerInstanceID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, value := range values {
|
||||
if !isProjectionOlder(freshness, value.Freshness) {
|
||||
value.Freshness = freshness
|
||||
value.UpdatedAt = svc.now()
|
||||
if err := svc.store.SCUMFlags().Update(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func scumPositionFromRow(serverID string, subjectType domain.SCUMProjectionSubject, subjectID string, row map[string]any, freshness domain.SCUMProjectionFreshnessState) (domain.SCUMCurrentPosition, bool) {
|
||||
x, hasX := firstFloat(row, "x", "worldX", "world_x", "locationX")
|
||||
y, hasY := firstFloat(row, "y", "worldY", "world_y", "locationY")
|
||||
z, hasZ := firstFloat(row, "z", "worldZ", "world_z", "locationZ")
|
||||
if !hasX || !hasY {
|
||||
return domain.SCUMCurrentPosition{}, false
|
||||
}
|
||||
if subjectID == "" {
|
||||
return domain.SCUMCurrentPosition{}, false
|
||||
}
|
||||
position := domain.SCUMCurrentPosition{ID: scumProjectionID("position-"+string(subjectType), serverID, subjectID), ServerInstanceID: serverID, SubjectType: subjectType, SubjectID: subjectID, MapID: coalesceString(firstString(row, "mapId", "map_id"), domain.SCUMMapTrajectoryMapID), MapVersion: coalesceString(firstString(row, "mapVersion", "map_version"), "0.9"), X: x, Y: y, HasCoordinates: true, LastSaveTime: firstTime(row, "lastSaveTime", "last_save_time"), Freshness: freshness}
|
||||
if hasZ && !math.IsNaN(z) {
|
||||
position.Z = z
|
||||
}
|
||||
return position, true
|
||||
}
|
||||
|
||||
func isProjectionOlder(next, current domain.SCUMProjectionFreshnessState) bool {
|
||||
if current.Status == "" || current.Status == domain.SCUMProjectionUnknown {
|
||||
return false
|
||||
}
|
||||
if next.Source == current.Source && next.QueryKey == current.QueryKey && next.Sequence > 0 && current.Sequence > 0 && next.Sequence < current.Sequence {
|
||||
return true
|
||||
}
|
||||
return !next.ObservedAt.IsZero() && !current.ObservedAt.IsZero() && next.ObservedAt.Before(current.ObservedAt)
|
||||
}
|
||||
|
||||
func scumObservationID(result domain.SCUMObservationResult) string {
|
||||
seed := fmt.Sprintf("%s/%s/%s/%d/%s", result.ServerInstanceID, result.PluginID, result.QueryKey, result.Sequence, result.Checksum)
|
||||
if result.Checksum == "" {
|
||||
seed = fmt.Sprintf("%s/%s/%s/%d/%s", result.ServerInstanceID, result.PluginID, result.QueryKey, result.Sequence, result.ObservedAt.Format(time.RFC3339Nano))
|
||||
}
|
||||
return "scum-observation-" + fingerprintID(result.ServerInstanceID, seed)
|
||||
}
|
||||
|
||||
func scumProjectionID(kind, serverID, subject string) string {
|
||||
return "scum-" + kind + "-" + fingerprintID(serverID, subject)
|
||||
}
|
||||
|
||||
func firstString(row map[string]any, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if value, ok := row[key]; ok {
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
if trimmed := strings.TrimSpace(typed); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
case fmt.Stringer:
|
||||
if trimmed := strings.TrimSpace(typed.String()); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
case int, int64, uint64, float64:
|
||||
return fmt.Sprint(typed)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func firstFloat(row map[string]any, keys ...string) (float64, bool) {
|
||||
for _, key := range keys {
|
||||
if value, ok := row[key]; ok {
|
||||
switch typed := value.(type) {
|
||||
case float64:
|
||||
return typed, true
|
||||
case float32:
|
||||
return float64(typed), true
|
||||
case int:
|
||||
return float64(typed), true
|
||||
case int64:
|
||||
return float64(typed), true
|
||||
case uint64:
|
||||
return float64(typed), true
|
||||
case string:
|
||||
parsed, err := strconv.ParseFloat(strings.TrimSpace(typed), 64)
|
||||
if err == nil {
|
||||
return parsed, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func firstInt(row map[string]any, keys ...string) (int, bool) {
|
||||
value, ok := firstFloat(row, keys...)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
return int(value), true
|
||||
}
|
||||
|
||||
func firstBool(row map[string]any, keys ...string) (bool, bool) {
|
||||
for _, key := range keys {
|
||||
if value, ok := row[key]; ok {
|
||||
switch typed := value.(type) {
|
||||
case bool:
|
||||
return typed, true
|
||||
case string:
|
||||
parsed, err := strconv.ParseBool(strings.TrimSpace(typed))
|
||||
if err == nil {
|
||||
return parsed, true
|
||||
}
|
||||
case int:
|
||||
return typed != 0, true
|
||||
case int64:
|
||||
return typed != 0, true
|
||||
case float64:
|
||||
return typed != 0, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
func firstTime(row map[string]any, keys ...string) time.Time {
|
||||
for _, key := range keys {
|
||||
if value, ok := row[key]; ok {
|
||||
switch typed := value.(type) {
|
||||
case time.Time:
|
||||
return typed
|
||||
case string:
|
||||
trimmed := strings.TrimSpace(typed)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if parsed, err := time.Parse(time.RFC3339Nano, trimmed); err == nil {
|
||||
return parsed
|
||||
}
|
||||
if parsed, err := time.Parse("2006-01-02 15:04:05", trimmed); err == nil {
|
||||
return parsed.UTC()
|
||||
}
|
||||
case int64:
|
||||
return time.Unix(typed, 0).UTC()
|
||||
case float64:
|
||||
return time.Unix(int64(typed), 0).UTC()
|
||||
}
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func unknownRowFields(row map[string]any, known ...string) map[string]any {
|
||||
knownSet := map[string]struct{}{}
|
||||
for _, key := range known {
|
||||
knownSet[key] = struct{}{}
|
||||
}
|
||||
unknown := map[string]any{}
|
||||
for key, value := range row {
|
||||
if _, ok := knownSet[key]; ok {
|
||||
continue
|
||||
}
|
||||
unknown[key] = value
|
||||
}
|
||||
if len(unknown) == 0 {
|
||||
return nil
|
||||
}
|
||||
return domain.CopyGameClientBridgePayload(unknown)
|
||||
}
|
||||
|
||||
func coalesceString(next, current string) string {
|
||||
if strings.TrimSpace(next) != "" {
|
||||
return strings.TrimSpace(next)
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
func coalesceTime(next, current time.Time) time.Time {
|
||||
if !next.IsZero() {
|
||||
return next
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
func limitSCUMProjectionSlice[T any](values *[]T, limit int) {
|
||||
if limit > 0 && len(*values) > limit {
|
||||
*values = (*values)[:limit]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
func TestSCUMObservationProjectsRealRowsAndSeparatesProfileFromSteamID(t *testing.T) {
|
||||
svc, _ := newRegisteredLogIngestService(t)
|
||||
observed := time.Date(2026, 8, 10, 9, 0, 0, 0, time.UTC)
|
||||
observation, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{
|
||||
ServerInstanceID: "server-1",
|
||||
PluginID: "server.scum",
|
||||
Source: "run.sqlite.read",
|
||||
QueryKey: "scum.player.profile",
|
||||
Sequence: 10,
|
||||
Checksum: "sha256:profile-10",
|
||||
ObservedAt: observed,
|
||||
Rows: []map[string]any{{
|
||||
"gamePlayerId": "steam-1",
|
||||
"userProfileId": "profile-99",
|
||||
"steamId": "steam-1",
|
||||
"displayName": "Moon",
|
||||
"squadId": "squad-1",
|
||||
"famePoints": 42,
|
||||
"normalBalance": 500.0,
|
||||
"goldBalance": 7.0,
|
||||
"x": 100,
|
||||
"y": 200,
|
||||
"z": 30,
|
||||
"lastSaveTime": observed.Add(-time.Minute).Format(time.RFC3339),
|
||||
"future_column": "preserved",
|
||||
}},
|
||||
})
|
||||
if err != nil || observation.Status != domain.SCUMObservationAccepted {
|
||||
t.Fatalf("apply observation=%+v err=%v", observation, err)
|
||||
}
|
||||
player, err := svc.store.GamePlayers().Get(gamePlayerRecordID("server-1", "steam-1"))
|
||||
if err != nil || player.DisplayName != "Moon" {
|
||||
t.Fatalf("expected game player from real row: player=%+v err=%v", player, err)
|
||||
}
|
||||
states, err := svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", UserProfileID: "profile-99"})
|
||||
if err != nil || len(states) != 1 {
|
||||
t.Fatalf("states=%+v err=%v", states, err)
|
||||
}
|
||||
state := states[0]
|
||||
if state.GamePlayerID != "steam-1" || state.UserProfileID != "profile-99" || state.SteamID != "steam-1" || state.NormalBalance != 500 || state.Online {
|
||||
t.Fatalf("identity/economy projection mixed IDs or inferred online incorrectly: %+v", state)
|
||||
}
|
||||
if !state.Position.HasCoordinates || state.Position.X != 100 || state.Position.Y != 200 || state.UnknownFields["future_column"] != "preserved" {
|
||||
t.Fatalf("position/unknown fields not projected safely: %+v", state)
|
||||
}
|
||||
stale, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: "server-1", PluginID: "server.scum", Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 9, Checksum: "sha256:profile-9", ObservedAt: observed.Add(-time.Hour), Rows: []map[string]any{{"gamePlayerId": "steam-1", "userProfileId": "profile-99", "displayName": "Old", "normalBalance": 9999}}})
|
||||
if err != nil || stale.Status != domain.SCUMObservationStale || stale.ErrorCode != "older_observation" {
|
||||
t.Fatalf("expected older observation stale, got %+v err=%v", stale, err)
|
||||
}
|
||||
again, err := svc.store.SCUMPlayerLiveStates().Get(state.ID)
|
||||
if err != nil || again.DisplayName != "Moon" || again.NormalBalance != 500 {
|
||||
t.Fatalf("older observation overwrote last-known-good: %+v err=%v", again, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMFailedObservationMarksStaleWithoutOverwritingProjection(t *testing.T) {
|
||||
svc, _ := newRegisteredLogIngestService(t)
|
||||
observed := time.Date(2026, 8, 10, 10, 0, 0, 0, time.UTC)
|
||||
if _, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: "server-1", PluginID: "server.scum", Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 1, Checksum: "sha256:ok", ObservedAt: observed, Rows: []map[string]any{{"gamePlayerId": "steam-2", "userProfileId": "profile-2", "displayName": "Nova", "normalBalance": 125}}}); err != nil {
|
||||
t.Fatalf("apply initial observation: %v", err)
|
||||
}
|
||||
failed, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: "server-1", PluginID: "server.scum", Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 2, Checksum: "sha256:failed", Status: domain.SCUMObservationFailed, ErrorCode: "sqlite_busy", ObservedAt: observed.Add(time.Minute)})
|
||||
if err != nil || failed.Status != domain.SCUMObservationFailed {
|
||||
t.Fatalf("failed observation=%+v err=%v", failed, err)
|
||||
}
|
||||
states, err := svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", GamePlayerID: "steam-2"})
|
||||
if err != nil || len(states) != 1 {
|
||||
t.Fatalf("states=%+v err=%v", states, err)
|
||||
}
|
||||
if states[0].NormalBalance != 125 || states[0].Freshness.Status != domain.SCUMProjectionStale || states[0].Freshness.StaleReason != "observation_failed" {
|
||||
t.Fatalf("failed query did not preserve values and mark stale: %+v", states[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMLoginLogsProjectLiveStateAndDatabaseSaveTimeDoesNotProveOnline(t *testing.T) {
|
||||
svc, token := newRegisteredLogIngestService(t)
|
||||
createLogStreamFixture(t, svc)
|
||||
base := time.Date(2026, 8, 10, 11, 0, 0, 0, time.UTC)
|
||||
login := gamePlayerBatch(t, token, 1, []domain.LogEntry{{Seq: 1, Timestamp: base, Line: "login accepted", Fields: map[string]string{"eventType": "scum.login", "playerId": "steam-3", "playerName": "Comet", "sessionId": "session-3", "outcome": "accepted"}}})
|
||||
if _, err := svc.IngestLogBatch(login); err != nil {
|
||||
t.Fatalf("ingest login: %v", err)
|
||||
}
|
||||
states, err := svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", GamePlayerID: "steam-3"})
|
||||
if err != nil || len(states) != 1 || !states[0].Online {
|
||||
t.Fatalf("login did not mark live state online: states=%+v err=%v", states, err)
|
||||
}
|
||||
logout := gamePlayerBatch(t, token, 2, []domain.LogEntry{{Seq: 2, Timestamp: base.Add(time.Minute), Line: "logout", Fields: map[string]string{"eventType": "scum.logout", "playerId": "steam-3", "playerName": "Comet", "sessionId": "session-3", "reason": "disconnect"}}})
|
||||
if _, err := svc.IngestLogBatch(logout); err != nil {
|
||||
t.Fatalf("ingest logout: %v", err)
|
||||
}
|
||||
if _, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: "server-1", PluginID: "server.scum", Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 3, Checksum: "sha256:save-time", ObservedAt: base.Add(2 * time.Minute), Rows: []map[string]any{{"gamePlayerId": "steam-3", "userProfileId": "profile-3", "displayName": "Comet", "lastSaveTime": base.Add(90 * time.Second).Format(time.RFC3339)}}}); err != nil {
|
||||
t.Fatalf("apply save-time observation: %v", err)
|
||||
}
|
||||
states, err = svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", GamePlayerID: "steam-3"})
|
||||
if err != nil || len(states) != 1 {
|
||||
t.Fatalf("states=%+v err=%v", states, err)
|
||||
}
|
||||
if states[0].Online || states[0].LastSaveTime.IsZero() {
|
||||
t.Fatalf("last_save_time was incorrectly treated as online proof: %+v", states[0])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/repo"
|
||||
)
|
||||
|
||||
type scumWorkflowTemplateDefinition struct {
|
||||
Key string
|
||||
Title string
|
||||
Steps []scumWorkflowStepDefinition
|
||||
}
|
||||
|
||||
type scumWorkflowStepDefinition struct {
|
||||
Key string
|
||||
DependsOn []string
|
||||
OperationKey string
|
||||
QueryTemplateKey string
|
||||
Capability string
|
||||
TargetKey string
|
||||
MutatesState bool
|
||||
MaxAttempts int
|
||||
Summary string
|
||||
}
|
||||
|
||||
func (svc *CoreService) CreateSCUMWorkflowForSession(sessionID, serverID string, request domain.SCUMWorkflowInstance) (domain.SCUMWorkflowInstance, error) {
|
||||
request = domain.CopySCUMWorkflowInstance(request)
|
||||
user, err := svc.GetCurrentUser(sessionID)
|
||||
if err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
if err := svc.authorizeServerLifecycle(sessionID, serverID); err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(serverID)
|
||||
if err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
template, ok := scumWorkflowTemplates()[request.TemplateKey]
|
||||
if !ok {
|
||||
return domain.SCUMWorkflowInstance{}, validationError("SCUM workflow template is not declared")
|
||||
}
|
||||
if strings.TrimSpace(request.IdempotencyKey) == "" || len(request.IdempotencyKey) > 120 {
|
||||
return domain.SCUMWorkflowInstance{}, validationError("workflow idempotency key is required")
|
||||
}
|
||||
if existing, err := svc.store.SCUMWorkflowInstances().List(domain.SCUMWorkflowInstanceFilter{ServerInstanceID: serverID, IdempotencyKey: request.IdempotencyKey}); err == nil && len(existing) > 0 {
|
||||
return domain.CopySCUMWorkflowInstance(existing[0]), nil
|
||||
} else if err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
stamp := svc.now()
|
||||
workflow := domain.SCUMWorkflowInstance{ID: "scum-workflow-" + fingerprintID(serverID, request.IdempotencyKey), ServerInstanceID: serverID, PluginID: plugin.ID, TemplateKey: template.Key, RequestedBy: user.ID, IdempotencyKey: request.IdempotencyKey, Status: domain.SCUMWorkflowQueued, Input: domain.CopyGameClientBridgePayload(request.Input), SafeSummary: domain.SCUMSafeSummary{Title: template.Title, Message: "SCUM workflow queued with typed steps and safe summaries."}, CreatedAt: stamp, UpdatedAt: stamp}
|
||||
if err := svc.store.SCUMWorkflowInstances().Create(workflow); err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
for index, step := range template.Steps {
|
||||
maxAttempts := step.MaxAttempts
|
||||
if maxAttempts == 0 {
|
||||
maxAttempts = 1
|
||||
}
|
||||
record := domain.SCUMWorkflowStep{ID: fmt.Sprintf("%s.step.%02d.%s", workflow.ID, index+1, step.Key), WorkflowID: workflow.ID, ServerInstanceID: serverID, StepKey: step.Key, DependsOn: domain.CopyStringSlice(step.DependsOn), Status: domain.SCUMWorkflowStepQueued, OperationKey: step.OperationKey, QueryTemplateKey: step.QueryTemplateKey, Capability: step.Capability, TargetKey: step.TargetKey, MaxAttempts: maxAttempts, MutatesState: step.MutatesState, SafeSummary: domain.SCUMSafeSummary{Title: step.Key, Message: step.Summary}, CreatedAt: stamp, UpdatedAt: stamp}
|
||||
if err := svc.store.SCUMWorkflowSteps().Create(record); err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
}
|
||||
_, err = svc.recordAuditEventWithID(user.ID, "scum.workflow.create", "scum-workflow", workflow.ID, domain.AuditResultQueued, "typed SCUM workflow queued")
|
||||
return domain.CopySCUMWorkflowInstance(workflow), err
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMWorkflowsForSession(sessionID string, filter domain.SCUMWorkflowInstanceFilter) ([]domain.SCUMWorkflowInstance, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := svc.store.SCUMWorkflowInstances().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitSCUMProjectionSlice(&values, filter.Limit)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMWorkflowStepsForSession(sessionID string, filter domain.SCUMWorkflowStepFilter) ([]domain.SCUMWorkflowStep, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := svc.store.SCUMWorkflowSteps().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitSCUMProjectionSlice(&values, filter.Limit)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) DispatchNextSCUMWorkflowSteps(serverID string, limit int) ([]domain.SCUMWorkflowStep, error) {
|
||||
if limit <= 0 {
|
||||
limit = 1
|
||||
}
|
||||
workflows, err := svc.store.SCUMWorkflowInstances().List(domain.SCUMWorkflowInstanceFilter{ServerInstanceID: serverID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.SliceStable(workflows, func(i, j int) bool {
|
||||
if workflows[i].CreatedAt.Equal(workflows[j].CreatedAt) {
|
||||
return workflows[i].IdempotencyKey < workflows[j].IdempotencyKey
|
||||
}
|
||||
return workflows[i].CreatedAt.Before(workflows[j].CreatedAt)
|
||||
})
|
||||
dispatched := []domain.SCUMWorkflowStep{}
|
||||
activeMutating, err := svc.hasActiveSCUMMutatingStep(serverID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, workflow := range workflows {
|
||||
if !scumWorkflowRunnable(workflow.Status) || len(dispatched) >= limit {
|
||||
continue
|
||||
}
|
||||
steps, err := svc.sortedSCUMWorkflowSteps(workflow.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, step := range steps {
|
||||
if len(dispatched) >= limit || !scumWorkflowStepRunnable(step.Status) || !scumWorkflowDependenciesConfirmed(step, steps) {
|
||||
continue
|
||||
}
|
||||
if step.MutatesState && activeMutating {
|
||||
return dispatched, nil
|
||||
}
|
||||
if blocked, err := svc.blockSCUMStepIfRunUnavailable(workflow, step); err != nil || blocked.ID != "" {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dispatched = append(dispatched, blocked)
|
||||
return dispatched, nil
|
||||
}
|
||||
step.Status = domain.SCUMWorkflowStepRunning
|
||||
step.Attempt++
|
||||
step.UpdatedAt = svc.now()
|
||||
if err := svc.store.SCUMWorkflowSteps().Update(step); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
workflow.Status = domain.SCUMWorkflowRunning
|
||||
workflow.CurrentStepKey = step.StepKey
|
||||
workflow.UpdatedAt = step.UpdatedAt
|
||||
if err := svc.store.SCUMWorkflowInstances().Update(workflow); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dispatched = append(dispatched, domain.CopySCUMWorkflowStep(step))
|
||||
if step.MutatesState {
|
||||
activeMutating = true
|
||||
return dispatched, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return dispatched, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) CompleteSCUMWorkflowStep(stepID string, status domain.SCUMWorkflowStepStatus, confirmation domain.SCUMOperationConfirmation) (domain.SCUMWorkflowInstance, error) {
|
||||
step, err := svc.store.SCUMWorkflowSteps().Get(stepID)
|
||||
if err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
workflow, err := svc.store.SCUMWorkflowInstances().Get(step.WorkflowID)
|
||||
if err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
if !scumWorkflowStepTerminal(status) {
|
||||
return domain.SCUMWorkflowInstance{}, validationError("SCUM workflow step completion status must be terminal")
|
||||
}
|
||||
stamp := svc.now()
|
||||
step.Status = status
|
||||
step.Confirmation = domain.CopySCUMOperationConfirmation(confirmation)
|
||||
step.CompletedAt = stamp
|
||||
step.UpdatedAt = stamp
|
||||
if err := svc.store.SCUMWorkflowSteps().Update(step); err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
return svc.refreshSCUMWorkflowStatus(workflow)
|
||||
}
|
||||
|
||||
func (svc *CoreService) RetrySCUMWorkflowStep(stepID string) (domain.SCUMWorkflowStep, error) {
|
||||
step, err := svc.store.SCUMWorkflowSteps().Get(stepID)
|
||||
if err != nil {
|
||||
return domain.SCUMWorkflowStep{}, err
|
||||
}
|
||||
workflow, err := svc.store.SCUMWorkflowInstances().Get(step.WorkflowID)
|
||||
if err != nil {
|
||||
return domain.SCUMWorkflowStep{}, err
|
||||
}
|
||||
if step.Attempt >= step.MaxAttempts {
|
||||
return domain.SCUMWorkflowStep{}, validationError("SCUM workflow step retry limit reached")
|
||||
}
|
||||
if step.MutatesState && step.Status == domain.SCUMWorkflowStepUnknown && step.Confirmation.Status != "confirmed" {
|
||||
step.SafeSummary = domain.SCUMSafeSummary{Title: "确认后才能重试", Message: "State-changing SCUM step is unknown; workflow must run confirmation/readback before retry to avoid duplicate effects."}
|
||||
step.UpdatedAt = svc.now()
|
||||
if err := svc.store.SCUMWorkflowSteps().Update(step); err != nil {
|
||||
return domain.SCUMWorkflowStep{}, err
|
||||
}
|
||||
return domain.CopySCUMWorkflowStep(step), nil
|
||||
}
|
||||
step.Status = domain.SCUMWorkflowStepQueued
|
||||
step.Confirmation = domain.SCUMOperationConfirmation{}
|
||||
step.CompletedAt = time.Time{}
|
||||
step.UpdatedAt = svc.now()
|
||||
if err := svc.store.SCUMWorkflowSteps().Update(step); err != nil {
|
||||
return domain.SCUMWorkflowStep{}, err
|
||||
}
|
||||
workflow.Status = domain.SCUMWorkflowQueued
|
||||
workflow.BlockerReason = ""
|
||||
workflow.UpdatedAt = step.UpdatedAt
|
||||
if err := svc.store.SCUMWorkflowInstances().Update(workflow); err != nil {
|
||||
return domain.SCUMWorkflowStep{}, err
|
||||
}
|
||||
return domain.CopySCUMWorkflowStep(step), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) sortedSCUMWorkflowSteps(workflowID string) ([]domain.SCUMWorkflowStep, error) {
|
||||
steps, err := svc.store.SCUMWorkflowSteps().List(domain.SCUMWorkflowStepFilter{WorkflowID: workflowID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.SliceStable(steps, func(i, j int) bool {
|
||||
if steps[i].CreatedAt.Equal(steps[j].CreatedAt) {
|
||||
return steps[i].ID < steps[j].ID
|
||||
}
|
||||
return steps[i].CreatedAt.Before(steps[j].CreatedAt)
|
||||
})
|
||||
return steps, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) hasActiveSCUMMutatingStep(serverID string) (bool, error) {
|
||||
mutates := true
|
||||
for _, status := range []domain.SCUMWorkflowStepStatus{domain.SCUMWorkflowStepRunning, domain.SCUMWorkflowStepConfirming} {
|
||||
steps, err := svc.store.SCUMWorkflowSteps().List(domain.SCUMWorkflowStepFilter{ServerInstanceID: serverID, Status: status, MutatesState: &mutates})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(steps) > 0 {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) blockSCUMStepIfRunUnavailable(workflow domain.SCUMWorkflowInstance, step domain.SCUMWorkflowStep) (domain.SCUMWorkflowStep, error) {
|
||||
if strings.TrimSpace(step.Capability) == "" {
|
||||
return domain.SCUMWorkflowStep{}, nil
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(workflow.ServerInstanceID)
|
||||
if err != nil {
|
||||
return domain.SCUMWorkflowStep{}, err
|
||||
}
|
||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
||||
if err != nil {
|
||||
if err == repo.ErrNotFound {
|
||||
return svc.blockSCUMWorkflowStep(workflow, step, "Run unavailable", "No bound run endpoint is available for this typed SCUM workflow step.")
|
||||
}
|
||||
return domain.SCUMWorkflowStep{}, err
|
||||
}
|
||||
if err := svc.validateRunnableEndpoint(endpoint, step.Capability); err != nil {
|
||||
return svc.blockSCUMWorkflowStep(workflow, step, "Run unavailable", "Bound run cannot currently claim the declared workflow capability.")
|
||||
}
|
||||
return domain.SCUMWorkflowStep{}, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) blockSCUMWorkflowStep(workflow domain.SCUMWorkflowInstance, step domain.SCUMWorkflowStep, title string, message string) (domain.SCUMWorkflowStep, error) {
|
||||
stamp := svc.now()
|
||||
step.Status = domain.SCUMWorkflowStepBlocked
|
||||
step.SafeSummary = domain.SCUMSafeSummary{Title: title, Message: message, Details: map[string]string{"stepKey": step.StepKey, "capability": step.Capability}}
|
||||
step.UpdatedAt = stamp
|
||||
workflow.Status = domain.SCUMWorkflowBlocked
|
||||
workflow.CurrentStepKey = step.StepKey
|
||||
workflow.BlockerReason = title
|
||||
workflow.SafeSummary = step.SafeSummary
|
||||
workflow.UpdatedAt = stamp
|
||||
if err := svc.store.SCUMWorkflowSteps().Update(step); err != nil {
|
||||
return domain.SCUMWorkflowStep{}, err
|
||||
}
|
||||
if err := svc.store.SCUMWorkflowInstances().Update(workflow); err != nil {
|
||||
return domain.SCUMWorkflowStep{}, err
|
||||
}
|
||||
return domain.CopySCUMWorkflowStep(step), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) refreshSCUMWorkflowStatus(workflow domain.SCUMWorkflowInstance) (domain.SCUMWorkflowInstance, error) {
|
||||
steps, err := svc.sortedSCUMWorkflowSteps(workflow.ID)
|
||||
if err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
allConfirmed := len(steps) > 0
|
||||
stamp := svc.now()
|
||||
for _, step := range steps {
|
||||
switch step.Status {
|
||||
case domain.SCUMWorkflowStepFailed:
|
||||
workflow.Status = domain.SCUMWorkflowFailed
|
||||
case domain.SCUMWorkflowStepUnknown:
|
||||
workflow.Status = domain.SCUMWorkflowUnknown
|
||||
case domain.SCUMWorkflowStepCancelled:
|
||||
workflow.Status = domain.SCUMWorkflowCancelled
|
||||
case domain.SCUMWorkflowStepConfirmed:
|
||||
default:
|
||||
allConfirmed = false
|
||||
}
|
||||
if workflow.Status == domain.SCUMWorkflowFailed || workflow.Status == domain.SCUMWorkflowUnknown || workflow.Status == domain.SCUMWorkflowCancelled {
|
||||
workflow.CurrentStepKey = step.StepKey
|
||||
workflow.CompletedAt = stamp
|
||||
workflow.UpdatedAt = stamp
|
||||
return domain.CopySCUMWorkflowInstance(workflow), svc.store.SCUMWorkflowInstances().Update(workflow)
|
||||
}
|
||||
}
|
||||
if allConfirmed {
|
||||
workflow.Status = domain.SCUMWorkflowConfirmed
|
||||
workflow.CurrentStepKey = ""
|
||||
workflow.CompletedAt = stamp
|
||||
} else {
|
||||
workflow.Status = domain.SCUMWorkflowQueued
|
||||
workflow.CurrentStepKey = ""
|
||||
}
|
||||
workflow.UpdatedAt = stamp
|
||||
if err := svc.store.SCUMWorkflowInstances().Update(workflow); err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
return domain.CopySCUMWorkflowInstance(workflow), nil
|
||||
}
|
||||
|
||||
func scumWorkflowDependenciesConfirmed(step domain.SCUMWorkflowStep, steps []domain.SCUMWorkflowStep) bool {
|
||||
if len(step.DependsOn) == 0 {
|
||||
return true
|
||||
}
|
||||
statuses := map[string]domain.SCUMWorkflowStepStatus{}
|
||||
for _, candidate := range steps {
|
||||
statuses[candidate.StepKey] = candidate.Status
|
||||
}
|
||||
for _, dependency := range step.DependsOn {
|
||||
if statuses[dependency] != domain.SCUMWorkflowStepConfirmed {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func scumWorkflowRunnable(status domain.SCUMWorkflowStatus) bool {
|
||||
switch status {
|
||||
case domain.SCUMWorkflowQueued, domain.SCUMWorkflowRunning, domain.SCUMWorkflowWaiting:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func scumWorkflowStepRunnable(status domain.SCUMWorkflowStepStatus) bool {
|
||||
switch status {
|
||||
case domain.SCUMWorkflowStepQueued, domain.SCUMWorkflowStepWaiting:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func scumWorkflowStepTerminal(status domain.SCUMWorkflowStepStatus) bool {
|
||||
switch status {
|
||||
case domain.SCUMWorkflowStepConfirmed, domain.SCUMWorkflowStepFailed, domain.SCUMWorkflowStepUnknown, domain.SCUMWorkflowStepCancelled:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func scumWorkflowTemplates() map[string]scumWorkflowTemplateDefinition {
|
||||
read := domain.JobCapabilityRemoteRunDBSQLiteQuery
|
||||
logs := domain.JobCapabilityRemoteRunLogsTransfer
|
||||
protectedSQL := domain.JobCapabilityRemoteRunProtectedSQL
|
||||
rcon := domain.JobCapabilityRemoteRunRCONCommand
|
||||
return map[string]scumWorkflowTemplateDefinition{
|
||||
"scum.bootstrap-real-data": {Key: "scum.bootstrap-real-data", Title: "Bootstrap SCUM real data", Steps: []scumWorkflowStepDefinition{{Key: "verify-run-binding", Capability: read, TargetKey: "scum-database", Summary: "Verify run binding and SCUM.db query capability."}, {Key: "schema-probe", DependsOn: []string{"verify-run-binding"}, Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.schema.probe", Summary: "Probe SCUM.db schema before projection refresh."}, {Key: "login-cursor", DependsOn: []string{"schema-probe"}, Capability: logs, TargetKey: "scum-login", Summary: "Initialize login log observation cursor."}}},
|
||||
"scum.player-refresh": {Key: "scum.player-refresh", Title: "Refresh SCUM player", Steps: []scumWorkflowStepDefinition{{Key: "login-evidence", Capability: logs, TargetKey: "scum-login", Summary: "Sync login/logout evidence."}, {Key: "player-profile", DependsOn: []string{"login-evidence"}, Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.player.profile", Summary: "Read player profile/economy facts."}, {Key: "position-read", DependsOn: []string{"player-profile"}, Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.positions", Summary: "Read current player coordinates."}}},
|
||||
"scum.world-refresh": {Key: "scum.world-refresh", Title: "Refresh SCUM world", Steps: []scumWorkflowStepDefinition{{Key: "squad-read", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.squads", MaxAttempts: 2, Summary: "Refresh squads."}, {Key: "vehicle-read", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.vehicles", MaxAttempts: 2, Summary: "Refresh vehicles."}, {Key: "flag-read", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.flags", MaxAttempts: 2, Summary: "Refresh flags."}, {Key: "position-read", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.positions", MaxAttempts: 2, Summary: "Refresh map positions."}}},
|
||||
"scum.player-correction": {Key: "scum.player-correction", Title: "SCUM player correction", Steps: []scumWorkflowStepDefinition{{Key: "safety-check", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.player.profile", Summary: "Verify current projection, before value, offline state, and backup evidence."}, {Key: "apply-operation", DependsOn: []string{"safety-check"}, Capability: protectedSQL, TargetKey: "scum-database", OperationKey: "player.attribute.855.set", MutatesState: true, Summary: "Apply the approved typed operation through Run."}, {Key: "confirmation-read", DependsOn: []string{"apply-operation"}, Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.player.profile", Summary: "Confirm the requested value by readback."}}},
|
||||
"scum.gift-delivery": {Key: "scum.gift-delivery", Title: "SCUM gift delivery", Steps: []scumWorkflowStepDefinition{{Key: "eligibility-check", Summary: "Evaluate gift eligibility and idempotency."}, {Key: "deliver-reward", DependsOn: []string{"eligibility-check"}, Capability: rcon, TargetKey: "scum-management", OperationKey: "reward.deliver", MutatesState: true, MaxAttempts: 2, Summary: "Deliver approved reward through typed operation."}, {Key: "notify-player", DependsOn: []string{"deliver-reward"}, Capability: rcon, TargetKey: "scum-management", OperationKey: "player.notify", MutatesState: true, Summary: "Notify the player after delivery."}, {Key: "confirmation-read", DependsOn: []string{"notify-player"}, Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.player.profile", Summary: "Confirm grant state/readback before marking delivered."}}},
|
||||
"scum.territory-audit": {Key: "scum.territory-audit", Title: "SCUM territory audit", Steps: []scumWorkflowStepDefinition{{Key: "squad-roster", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.squad-members", Summary: "Refresh squad rosters."}, {Key: "flag-ownership", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.flags", Summary: "Refresh flag ownership."}, {Key: "risk-signal", DependsOn: []string{"squad-roster", "flag-ownership"}, Summary: "Project stale owner/member risk signals."}}},
|
||||
"scum.vehicle-audit": {Key: "scum.vehicle-audit", Title: "SCUM vehicle audit", Steps: []scumWorkflowStepDefinition{{Key: "vehicle-read", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.vehicles", Summary: "Refresh vehicle inventory."}, {Key: "vehicle-map", DependsOn: []string{"vehicle-read"}, Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.positions", Summary: "Refresh vehicle map overlays."}}},
|
||||
"scum.ai-assist": {Key: "scum.ai-assist", Title: "SCUM AI assist", Steps: []scumWorkflowStepDefinition{{Key: "collect-allowed-fields", Summary: "Collect plugin-declared config fields and workflow inputs."}, {Key: "draft-review", DependsOn: []string{"collect-allowed-fields"}, Summary: "Create a reviewable typed diff or workflow draft."}, {Key: "approved-dispatch", DependsOn: []string{"draft-review"}, MutatesState: true, Summary: "Dispatch only after human approval through typed paths."}}},
|
||||
"scum.product-cleanup": {Key: "scum.product-cleanup", Title: "SCUM product cleanup", Steps: []scumWorkflowStepDefinition{{Key: "remove-raw-routes", Summary: "Remove raw logs, terminal, config, and operation-history product routes."}, {Key: "publish-safe-status", DependsOn: []string{"remove-raw-routes"}, Summary: "Route users to safe workflow/status surfaces."}}},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/repo"
|
||||
)
|
||||
|
||||
func TestSCUMWorkflowDispatchesReadStepsWithBoundedConcurrencyAndIdempotency(t *testing.T) {
|
||||
svc, session, instance := newSCUMWorkflowFixture(t, true)
|
||||
workflow, err := svc.CreateSCUMWorkflowForSession(session, instance.ID, domain.SCUMWorkflowInstance{TemplateKey: "scum.world-refresh", IdempotencyKey: "world-refresh-1", Input: map[string]any{"scope": "world"}})
|
||||
if err != nil || workflow.Status != domain.SCUMWorkflowQueued {
|
||||
t.Fatalf("create world workflow=%+v err=%v", workflow, err)
|
||||
}
|
||||
duplicate, err := svc.CreateSCUMWorkflowForSession(session, instance.ID, domain.SCUMWorkflowInstance{TemplateKey: "scum.world-refresh", IdempotencyKey: "world-refresh-1"})
|
||||
if err != nil || duplicate.ID != workflow.ID {
|
||||
t.Fatalf("expected idempotent workflow create: duplicate=%+v err=%v", duplicate, err)
|
||||
}
|
||||
dispatched, err := svc.DispatchNextSCUMWorkflowSteps(instance.ID, 3)
|
||||
if err != nil || len(dispatched) != 3 {
|
||||
t.Fatalf("expected three bounded read steps dispatched: steps=%+v err=%v", dispatched, err)
|
||||
}
|
||||
for _, step := range dispatched {
|
||||
if step.MutatesState || step.Status != domain.SCUMWorkflowStepRunning || step.Attempt != 1 {
|
||||
t.Fatalf("unexpected read step dispatch: %+v", step)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMWorkflowSerializesMutatingStepsPerServer(t *testing.T) {
|
||||
svc, session, instance := newSCUMWorkflowFixture(t, true)
|
||||
first, err := svc.CreateSCUMWorkflowForSession(session, instance.ID, domain.SCUMWorkflowInstance{TemplateKey: "scum.gift-delivery", IdempotencyKey: "gift-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("create first gift workflow: %v", err)
|
||||
}
|
||||
if _, err := svc.CreateSCUMWorkflowForSession(session, instance.ID, domain.SCUMWorkflowInstance{TemplateKey: "scum.gift-delivery", IdempotencyKey: "gift-2"}); err != nil {
|
||||
t.Fatalf("create second gift workflow: %v", err)
|
||||
}
|
||||
steps, err := svc.DispatchNextSCUMWorkflowSteps(instance.ID, 1)
|
||||
if err != nil || len(steps) != 1 || steps[0].StepKey != "eligibility-check" {
|
||||
t.Fatalf("expected first eligibility step: steps=%+v err=%v", steps, err)
|
||||
}
|
||||
if _, err := svc.CompleteSCUMWorkflowStep(steps[0].ID, domain.SCUMWorkflowStepConfirmed, domain.SCUMOperationConfirmation{Status: "confirmed"}); err != nil {
|
||||
t.Fatalf("complete eligibility: %v", err)
|
||||
}
|
||||
steps, err = svc.DispatchNextSCUMWorkflowSteps(instance.ID, 1)
|
||||
if err != nil || len(steps) != 1 || steps[0].StepKey != "deliver-reward" || !steps[0].MutatesState {
|
||||
t.Fatalf("expected first mutating reward step: steps=%+v err=%v", steps, err)
|
||||
}
|
||||
if steps[0].WorkflowID != first.ID {
|
||||
t.Fatalf("expected first workflow to keep the mutation slot: step=%+v first=%+v", steps[0], first)
|
||||
}
|
||||
blockedByActiveMutation, err := svc.DispatchNextSCUMWorkflowSteps(instance.ID, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("dispatch while mutation active: %v", err)
|
||||
}
|
||||
for _, step := range blockedByActiveMutation {
|
||||
if step.MutatesState {
|
||||
t.Fatalf("second state-changing step should wait for first terminal state: steps=%+v", blockedByActiveMutation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMWorkflowBlocksWhenRunUnavailable(t *testing.T) {
|
||||
svc, session, instance := newSCUMWorkflowFixture(t, false)
|
||||
workflow, err := svc.CreateSCUMWorkflowForSession(session, instance.ID, domain.SCUMWorkflowInstance{TemplateKey: "scum.player-refresh", IdempotencyKey: "player-refresh-blocked"})
|
||||
if err != nil {
|
||||
t.Fatalf("create player refresh workflow: %v", err)
|
||||
}
|
||||
steps, err := svc.DispatchNextSCUMWorkflowSteps(instance.ID, 1)
|
||||
if err != nil || len(steps) != 1 || steps[0].Status != domain.SCUMWorkflowStepBlocked {
|
||||
t.Fatalf("expected blocked run step: steps=%+v err=%v", steps, err)
|
||||
}
|
||||
updated, err := svc.store.SCUMWorkflowInstances().Get(workflow.ID)
|
||||
if err != nil || updated.Status != domain.SCUMWorkflowBlocked || strings.Contains(updated.SafeSummary.Message, "/") || strings.Contains(strings.ToLower(updated.SafeSummary.Message), "token") {
|
||||
t.Fatalf("workflow blocker should be safe: workflow=%+v err=%v", updated, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMWorkflowRetryRequiresConfirmationAfterUnknownMutation(t *testing.T) {
|
||||
svc, session, instance := newSCUMWorkflowFixture(t, true)
|
||||
if _, err := svc.CreateSCUMWorkflowForSession(session, instance.ID, domain.SCUMWorkflowInstance{TemplateKey: "scum.gift-delivery", IdempotencyKey: "gift-unknown"}); err != nil {
|
||||
t.Fatalf("create gift workflow: %v", err)
|
||||
}
|
||||
steps, err := svc.DispatchNextSCUMWorkflowSteps(instance.ID, 1)
|
||||
if err != nil || len(steps) != 1 {
|
||||
t.Fatalf("dispatch eligibility: steps=%+v err=%v", steps, err)
|
||||
}
|
||||
if _, err := svc.CompleteSCUMWorkflowStep(steps[0].ID, domain.SCUMWorkflowStepConfirmed, domain.SCUMOperationConfirmation{Status: "confirmed"}); err != nil {
|
||||
t.Fatalf("complete eligibility: %v", err)
|
||||
}
|
||||
steps, err = svc.DispatchNextSCUMWorkflowSteps(instance.ID, 1)
|
||||
if err != nil || len(steps) != 1 || !steps[0].MutatesState {
|
||||
t.Fatalf("dispatch mutating reward: steps=%+v err=%v", steps, err)
|
||||
}
|
||||
if _, err := svc.CompleteSCUMWorkflowStep(steps[0].ID, domain.SCUMWorkflowStepUnknown, domain.SCUMOperationConfirmation{Status: "unknown"}); err != nil {
|
||||
t.Fatalf("complete unknown mutation: %v", err)
|
||||
}
|
||||
retry, err := svc.RetrySCUMWorkflowStep(steps[0].ID)
|
||||
if err != nil || retry.Status != domain.SCUMWorkflowStepUnknown || !strings.Contains(retry.SafeSummary.Title, "确认") {
|
||||
t.Fatalf("unknown mutating retry should require confirmation: step=%+v err=%v", retry, err)
|
||||
}
|
||||
}
|
||||
|
||||
func newSCUMWorkflowFixture(t *testing.T, runAvailable bool) (*CoreService, string, domain.ServerInstance) {
|
||||
t.Helper()
|
||||
svc := newCoreService(repo.NewMemoryStore(), func() time.Time { return fixedTime })
|
||||
capabilities := []string{domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunProtectedSQL, domain.JobCapabilityRemoteRunRCONCommand}
|
||||
plugin, err := svc.CreateGamePlugin(domain.GamePlugin{ID: "server.scum", Name: "SCUM", Version: "1.0.0", ServerType: "scum", ManifestRef: "artifact://manifests/server.scum/1.0.0", CreateFormSchemaRef: "artifact://schemas/server.scum/create-form/1.0.0", RequiredRunCapabilities: capabilities, DeclaredPermissions: []string{"server.game-client.read", "server.game-client.command", "server.game-client.maintenance"}, Permissions: domain.PluginPermissions{Jobs: true, RemoteAccess: true}, RemoteAccess: domain.GamePluginRemoteAccess{Methods: []string{"run"}, RunCapabilities: capabilities, DatabaseEngines: []string{"sqlite"}, RCON: true, LogTransfer: true}, LifecycleActions: domain.PluginLifecycleActions{Start: "actions/start.json"}, RuntimeProfiles: domain.GamePluginRuntimeProfiles{TransportProfiles: []domain.RuntimeTransportProfile{{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunProtectedSQL}}, {Key: "scum-management", Kind: "rcon", TargetKey: "scum-management", Capabilities: []string{domain.JobCapabilityRemoteRunRCONCommand}}}}})
|
||||
if err != nil {
|
||||
t.Fatalf("create workflow plugin: %v", err)
|
||||
}
|
||||
endpoint, err := svc.CreateRunEndpoint(domain.RunEndpoint{ID: "run-local", DisplayName: "Local Run", Version: "0.1.0", Platform: "windows", Architecture: "amd64", Status: domain.RunEndpointStatusOnline, Capabilities: capabilities, Capacity: domain.RunCapacity{MaxJobs: 4}, LastHeartbeatAt: fixedTime})
|
||||
if err != nil {
|
||||
t.Fatalf("create workflow endpoint: %v", err)
|
||||
}
|
||||
session := createServiceUserAndLogin(t, svc, domain.User{ID: "workflow-owner", DisplayName: "Workflow Owner", Email: "workflow-owner@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
|
||||
instance, err := svc.CreateServerInstanceForSession(session, domain.ServerInstance{ID: "server-workflow", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Workflow Server", State: domain.ServerInstanceStateRunning})
|
||||
if err != nil {
|
||||
t.Fatalf("create workflow server: %v", err)
|
||||
}
|
||||
if !runAvailable {
|
||||
endpoint.Status = domain.RunEndpointStatusOffline
|
||||
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
|
||||
t.Fatalf("mark workflow endpoint offline: %v", err)
|
||||
}
|
||||
}
|
||||
return svc, session, instance
|
||||
}
|
||||
@@ -51,31 +51,6 @@ func ValidateRunJobResult(result domain.RunJobResult) error {
|
||||
if result.ExecutionResult.Checksum != "" && !validSHA256Checksum(result.ExecutionResult.Checksum) {
|
||||
violations = append(violations, "executionResult.checksum must be sha256:<hex>")
|
||||
}
|
||||
if result.ExecutionResult.SQLiteSchemaProbe != nil {
|
||||
if err := ValidateSCUMSchemaProbeResult(*result.ExecutionResult.SQLiteSchemaProbe); err != nil {
|
||||
violations = append(violations, "executionResult.sqliteSchemaProbe: "+err.Error())
|
||||
}
|
||||
}
|
||||
if result.ExecutionResult.SQLiteTemplate != nil {
|
||||
if err := ValidateSCUMSQLiteTemplateResult(*result.ExecutionResult.SQLiteTemplate); err != nil {
|
||||
violations = append(violations, "executionResult.sqliteTemplate: "+err.Error())
|
||||
}
|
||||
}
|
||||
if result.ExecutionResult.RCONTemplate != nil {
|
||||
if err := ValidateSCUMTypedRCONTemplateResult(*result.ExecutionResult.RCONTemplate); err != nil {
|
||||
violations = append(violations, "executionResult.rconTemplate: "+err.Error())
|
||||
}
|
||||
}
|
||||
if result.ExecutionResult.GuardedMutation != nil {
|
||||
if err := ValidateSCUMGuardedMutationResult(*result.ExecutionResult.GuardedMutation); err != nil {
|
||||
violations = append(violations, "executionResult.guardedMutation: "+err.Error())
|
||||
}
|
||||
}
|
||||
if result.ExecutionResult.ParsedLogBatch != nil {
|
||||
if err := ValidateSCUMParsedLogBatchResult(*result.ExecutionResult.ParsedLogBatch); err != nil {
|
||||
violations = append(violations, "executionResult.parsedLogBatch: "+err.Error())
|
||||
}
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
|
||||
@@ -157,7 +157,6 @@ func ValidateGamePlugin(plugin domain.GamePlugin) error {
|
||||
violations = append(violations, validateRuntimeProfileCapabilityDeclarations(plugin.RuntimeProfiles, plugin.RequiredRunCapabilities)...)
|
||||
violations = append(violations, validateRuntimeLogEventPermissionDeclarations("runtimeProfiles.logEvents", plugin.RuntimeProfiles, plugin.DeclaredPermissions)...)
|
||||
violations = append(violations, validateGameClientBridgeManifest("gameClientBridge", plugin.GameClientBridge, plugin.DeclaredPermissions, plugin.Pages, plugin.RuntimeProfiles)...)
|
||||
violations = append(violations, validateSCUMLiveDataManifest("scumLiveData", plugin.SCUMLiveData, plugin.RequiredRunCapabilities, plugin.RemoteAccess, plugin.RuntimeProfiles)...)
|
||||
violations = append(violations, validateMapTrajectoryDeclaration("mapTrajectories", plugin.MapTrajectories)...)
|
||||
violations = append(violations, validatePluginCreateFields("createFields", plugin.CreateFields)...)
|
||||
violations = append(violations, validatePluginAssetFiles("lifecycleAssets", plugin.LifecycleAssets)...)
|
||||
@@ -230,7 +229,6 @@ func ValidateGamePluginManifestRegistration(registration domain.GamePluginManife
|
||||
violations = append(violations, validateRuntimeProfileCapabilityDeclarations(manifest.RuntimeProfiles, manifest.Capabilities)...)
|
||||
violations = append(violations, validateRuntimeLogEventPermissionDeclarations("manifest.runtimeProfiles.logEvents", manifest.RuntimeProfiles, manifest.Permissions)...)
|
||||
violations = append(violations, validateGameClientBridgeManifest("manifest.gameClientBridge", manifest.GameClientBridge, manifest.Permissions, manifest.Pages, manifest.RuntimeProfiles)...)
|
||||
violations = append(violations, validateSCUMLiveDataManifest("manifest.scumLiveData", manifest.SCUMLiveData, manifest.Capabilities, manifest.RemoteAccess, manifest.RuntimeProfiles)...)
|
||||
violations = append(violations, validateMapTrajectoryDeclaration("manifest.mapTrajectories", manifest.MapTrajectories)...)
|
||||
violations = append(violations, validatePluginAssetFileDeclarations("manifest.assetFiles", manifest.AssetFiles)...)
|
||||
violations = append(violations, validatePluginAssetFiles("assetFiles", registration.AssetFiles)...)
|
||||
@@ -1533,39 +1531,6 @@ func ValidateJob(job domain.Job) error {
|
||||
violations = append(violations, "executionInput.sourceRcon must not persist adapter inputs")
|
||||
}
|
||||
}
|
||||
if job.ExecutionInput.SQLiteTemplate != nil {
|
||||
if err := ValidateSCUMSQLiteTemplateRequest(*job.ExecutionInput.SQLiteTemplate); err != nil {
|
||||
violations = append(violations, "executionInput.sqliteTemplate: "+err.Error())
|
||||
}
|
||||
if job.Capability != domain.JobCapabilityRemoteRunDBSQLiteQuery {
|
||||
violations = append(violations, "executionInput.sqliteTemplate is allowed only for sqlite query jobs")
|
||||
}
|
||||
}
|
||||
if job.ExecutionInput.RCONTemplate != nil {
|
||||
if err := ValidateSCUMTypedRCONTemplateRequest(*job.ExecutionInput.RCONTemplate); err != nil {
|
||||
violations = append(violations, "executionInput.rconTemplate: "+err.Error())
|
||||
}
|
||||
if job.Capability != domain.JobCapabilityRemoteRunProtectedRCON {
|
||||
violations = append(violations, "executionInput.rconTemplate is allowed only for protected rcon jobs")
|
||||
}
|
||||
if job.ExecutionInput.SourceRCON == nil || job.ExecutionInput.RemoteAdapterKind != "protected-rcon" {
|
||||
violations = append(violations, "executionInput.rconTemplate requires a protected rcon transport plan")
|
||||
}
|
||||
}
|
||||
if job.ExecutionInput.GuardedMutation != nil {
|
||||
if err := ValidateSCUMGuardedMutationRequest(*job.ExecutionInput.GuardedMutation); err != nil {
|
||||
violations = append(violations, "executionInput.guardedMutation: "+err.Error())
|
||||
}
|
||||
if job.Capability != domain.JobCapabilityRemoteRunProtectedSQL {
|
||||
violations = append(violations, "executionInput.guardedMutation is allowed only for protected sql jobs")
|
||||
}
|
||||
if job.ExecutionInput.RemoteAdapterKind != "protected-sql" {
|
||||
violations = append(violations, "executionInput.guardedMutation requires a protected sql transport plan")
|
||||
}
|
||||
if len(job.ExecutionInput.Inputs) != 0 || job.ExecutionInput.Content != "" {
|
||||
violations = append(violations, "executionInput.guardedMutation must not persist raw adapter inputs")
|
||||
}
|
||||
}
|
||||
violations = append(violations, validateRemoteAdapterInputs("executionInput.inputs", job.ExecutionInput.Inputs)...)
|
||||
if job.ExecutionResult.Checksum != "" && !validSHA256Checksum(job.ExecutionResult.Checksum) {
|
||||
violations = append(violations, "executionResult.checksum must be sha256:<hex>")
|
||||
@@ -1576,31 +1541,6 @@ func ValidateJob(job domain.Job) error {
|
||||
if len(job.ExecutionResult.AuditSummary) > maxAuditSummaryLength {
|
||||
violations = append(violations, "executionResult.auditSummary is too long")
|
||||
}
|
||||
if job.ExecutionResult.SQLiteSchemaProbe != nil {
|
||||
if err := ValidateSCUMSchemaProbeResult(*job.ExecutionResult.SQLiteSchemaProbe); err != nil {
|
||||
violations = append(violations, "executionResult.sqliteSchemaProbe: "+err.Error())
|
||||
}
|
||||
}
|
||||
if job.ExecutionResult.SQLiteTemplate != nil {
|
||||
if err := ValidateSCUMSQLiteTemplateResult(*job.ExecutionResult.SQLiteTemplate); err != nil {
|
||||
violations = append(violations, "executionResult.sqliteTemplate: "+err.Error())
|
||||
}
|
||||
}
|
||||
if job.ExecutionResult.RCONTemplate != nil {
|
||||
if err := ValidateSCUMTypedRCONTemplateResult(*job.ExecutionResult.RCONTemplate); err != nil {
|
||||
violations = append(violations, "executionResult.rconTemplate: "+err.Error())
|
||||
}
|
||||
}
|
||||
if job.ExecutionResult.GuardedMutation != nil {
|
||||
if err := ValidateSCUMGuardedMutationResult(*job.ExecutionResult.GuardedMutation); err != nil {
|
||||
violations = append(violations, "executionResult.guardedMutation: "+err.Error())
|
||||
}
|
||||
}
|
||||
if job.ExecutionResult.ParsedLogBatch != nil {
|
||||
if err := ValidateSCUMParsedLogBatchResult(*job.ExecutionResult.ParsedLogBatch); err != nil {
|
||||
violations = append(violations, "executionResult.parsedLogBatch: "+err.Error())
|
||||
}
|
||||
}
|
||||
if job.Capability == domain.JobCapabilityConfigWrite || job.Capability == domain.JobCapabilityFilesRead || job.Capability == domain.JobCapabilityFilesWrite {
|
||||
if job.ServerInstanceID == "" {
|
||||
violations = append(violations, "serverInstanceId is required for scoped file jobs")
|
||||
@@ -2259,7 +2199,7 @@ func validPluginRunCapability(capability string) bool {
|
||||
domain.JobCapabilityRemoteRsyncRead, domain.JobCapabilityRemoteRsyncWrite,
|
||||
domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunFilesWrite,
|
||||
domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop,
|
||||
domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteProbe, domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand,
|
||||
domain.JobCapabilityRemoteRunProtectedSQL, domain.JobCapabilityRemoteRunProtectedRCON, domain.JobCapabilityRemoteRunProgram,
|
||||
domain.JobCapabilityRunSelfUpdate, domain.JobCapabilityDependenciesCheck, domain.JobCapabilityDependenciesInstall,
|
||||
|
||||
@@ -295,29 +295,6 @@ func TestValidateGamePluginManifestRegistrationRejectsUnsafeCapabilitiesAndPermi
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGamePluginManifestRegistrationValidatesSCUMLiveDataGate(t *testing.T) {
|
||||
registration := validGamePluginManifestRegistration()
|
||||
registration.Manifest.ID = "game.scum"
|
||||
registration.Manifest.Capabilities = append(registration.Manifest.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe, domain.JobCapabilityRemoteRunFilesRead)
|
||||
registration.Manifest.RemoteAccess = domain.GamePluginRemoteAccess{Methods: []string{"run"}, RunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}}
|
||||
registration.Manifest.RuntimeProfiles.TransportProfiles = []domain.RuntimeTransportProfile{
|
||||
{Key: "server-files", Kind: "file", TargetKey: "server-root", Capabilities: []string{domain.JobCapabilityRemoteRunFilesRead}},
|
||||
{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}},
|
||||
}
|
||||
registration.Manifest.RuntimeProfiles.DataTargets = []domain.RuntimeDataTarget{{Key: "scum-database", Kind: "sqlite.snapshot", TransportKey: "scum-database", SourceRootKey: "server-root", SourcePath: "SCUM/Saved/SaveFiles/SCUM.db", WorkspaceKey: "databases/scum-database", RefreshPolicy: "on-demand-snapshot", MaxBytes: 1024 * 1024 * 1024, Platforms: []string{"windows"}}}
|
||||
registration.Manifest.SCUMLiveData = domain.SCUMLiveDataManifest{SchemaVersion: "1", Probe: domain.SCUMSchemaProbeDeclaration{Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, TargetKey: "scum-database", Bounds: domain.DefaultSCUMSchemaProbeBounds()}, CapabilityGates: []domain.SCUMLiveDataCapabilityGateDeclaration{{Capability: domain.SCUMDataCapabilityPlayerRead, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "scum-live-data-v0", EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "等待当前服务证据。"}}}
|
||||
|
||||
if err := ValidateGamePluginManifestRegistration(registration); err != nil {
|
||||
t.Fatalf("expected disabled live-data gate to validate, got %v", err)
|
||||
}
|
||||
|
||||
registration.Manifest.SCUMLiveData.CapabilityGates[0].Gate = domain.SCUMCapabilityGateEnabled
|
||||
err := ValidateGamePluginManifestRegistration(registration)
|
||||
if err == nil || !strings.Contains(err.Error(), "evidenceStatus must be compatible") || !strings.Contains(err.Error(), "requiredSchemaFingerprint") {
|
||||
t.Fatalf("expected enabled gate evidence violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateServerInstanceDependencies(t *testing.T) {
|
||||
instance := domain.ServerInstance{
|
||||
ID: "server-1",
|
||||
|
||||
@@ -34,8 +34,6 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
||||
logSourceRetentions := map[string]int{}
|
||||
logEventKeys := map[string]struct{}{}
|
||||
logEventTypes := map[string]struct{}{}
|
||||
dataTargetKeys := map[string]struct{}{}
|
||||
transportTargetKeys := map[string]struct{}{}
|
||||
|
||||
for i, probe := range profiles.Discovery {
|
||||
prefix := fmt.Sprintf("runtimeProfiles.discovery[%d]", i)
|
||||
@@ -303,7 +301,6 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
||||
}
|
||||
if transport.TargetKey != "" {
|
||||
violations = append(violations, validateProfileKey(prefix+".targetKey", transport.TargetKey)...)
|
||||
transportTargetKeys[transport.TargetKey] = struct{}{}
|
||||
}
|
||||
if len(transport.Capabilities) == 0 {
|
||||
violations = append(violations, prefix+".capabilities must not be empty")
|
||||
@@ -315,35 +312,6 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
||||
}
|
||||
violations = append(violations, duplicateViolations(prefix+".capabilities", transport.Capabilities)...)
|
||||
}
|
||||
for i, target := range profiles.DataTargets {
|
||||
prefix := fmt.Sprintf("runtimeProfiles.dataTargets[%d]", i)
|
||||
violations = append(violations, validateProfileKey(prefix+".key", target.Key)...)
|
||||
violations = append(violations, recordRuntimeProfileKey(dataTargetKeys, prefix+".key", target.Key)...)
|
||||
if target.Kind != "sqlite.snapshot" {
|
||||
violations = append(violations, prefix+".kind is invalid")
|
||||
}
|
||||
violations = append(violations, validateProfileKey(prefix+".transportKey", target.TransportKey)...)
|
||||
transport, transportExists := transportByKey(profiles.TransportProfiles, target.TransportKey)
|
||||
if !transportExists || transport.Kind != "sqlite" {
|
||||
violations = append(violations, prefix+".transportKey must reference a declared sqlite transport")
|
||||
}
|
||||
violations = append(violations, validateProfileKey(prefix+".sourceRootKey", target.SourceRootKey)...)
|
||||
if _, exists := transportTargetKeys[target.SourceRootKey]; !exists {
|
||||
violations = append(violations, prefix+".sourceRootKey must reference a declared runtime transport target")
|
||||
}
|
||||
violations = append(violations, validateSafeRelativeRuntimePath(prefix+".sourcePath", target.SourcePath)...)
|
||||
violations = append(violations, validateSafeRelativeRuntimePath(prefix+".workspaceKey", target.WorkspaceKey)...)
|
||||
if !strings.HasPrefix(target.WorkspaceKey, "databases/") {
|
||||
violations = append(violations, prefix+".workspaceKey must live under databases/")
|
||||
}
|
||||
if target.RefreshPolicy != "on-demand-snapshot" {
|
||||
violations = append(violations, prefix+".refreshPolicy is invalid")
|
||||
}
|
||||
if target.MaxBytes < 1 || target.MaxBytes > 1024*1024*1024 {
|
||||
violations = append(violations, prefix+".maxBytes is invalid")
|
||||
}
|
||||
violations = append(violations, validateRuntimePlatforms(prefix+".platforms", target.Platforms)...)
|
||||
}
|
||||
for i, manager := range profiles.ClientManagers {
|
||||
prefix := fmt.Sprintf("runtimeProfiles.clientManagers[%d]", i)
|
||||
violations = append(violations, validateProfileKey(prefix+".key", manager.Key)...)
|
||||
@@ -688,15 +656,6 @@ func recordRuntimeProfileKey(seen map[string]struct{}, field, key string) []stri
|
||||
return nil
|
||||
}
|
||||
|
||||
func transportByKey(transports []domain.RuntimeTransportProfile, key string) (domain.RuntimeTransportProfile, bool) {
|
||||
for _, transport := range transports {
|
||||
if transport.Key == key {
|
||||
return transport, true
|
||||
}
|
||||
}
|
||||
return domain.RuntimeTransportProfile{}, false
|
||||
}
|
||||
|
||||
func validateRuntimeProfileCapabilityDeclarations(profiles domain.GamePluginRuntimeProfiles, declared []string) []string {
|
||||
declaredSet := map[string]struct{}{}
|
||||
for _, capability := range declared {
|
||||
|
||||
@@ -1,981 +0,0 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
const (
|
||||
maxSCUMProbeObjects = 512
|
||||
maxSCUMProbeColumnsPerObject = 256
|
||||
maxSCUMProbeIndexesPerObject = 128
|
||||
maxSCUMProbeForeignKeys = 128
|
||||
maxSCUMProbeSamples = 3
|
||||
maxSCUMProbeTimeoutMS = 10000
|
||||
maxSCUMProbeResultBytes = 1024 * 1024
|
||||
maxSCUMTemplateParameters = 64
|
||||
maxSCUMTemplateRows = 1000
|
||||
maxSCUMTemplateBusyTimeoutMS = 1000
|
||||
maxSCUMTemplateValueBytes = 4096
|
||||
maxSCUMRCONPayloadBytes = 4096
|
||||
maxSCUMRCONResponseBytes = 64 * 1024
|
||||
maxSCUMRCONConfirmRecords = 128
|
||||
maxSCUMMutationPayloadBytes = 4096
|
||||
maxSCUMMutationReadbackBytes = 64 * 1024
|
||||
maxSCUMParsedLogEvents = 1024
|
||||
maxSCUMParsedLogPayloadBytes = 64 * 1024
|
||||
maxSCUMParsedLogLineBytes = 64 * 1024
|
||||
maxSCUMParsedLogResultBytes = 1024 * 1024
|
||||
)
|
||||
|
||||
var scumHashPattern = regexp.MustCompile(`^sha256:[a-fA-F0-9]{64}$|^[a-fA-F0-9]{16,128}$`)
|
||||
var scumDataTargetSafeErrorPattern = regexp.MustCompile(`^data_target_[a-z0-9_]{1,80}$`)
|
||||
var scumTemplateKeyPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_.-]{0,127}$`)
|
||||
|
||||
func ValidateSCUMSchemaProbeRequest(request domain.SCUMSchemaProbeRequest) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", request.RequestID)
|
||||
violations = appendRequired(violations, "jobId", request.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", request.Binding)...)
|
||||
violations = append(violations, validateSCUMSchemaProbeBounds("bounds", request.Bounds)...)
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMSQLiteTemplateRequest(request domain.SCUMSQLiteTemplateRequest) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", request.RequestID)
|
||||
violations = appendRequired(violations, "jobId", request.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", request.Binding)...)
|
||||
if !validSCUMReadCapability(request.Capability) {
|
||||
violations = append(violations, "capability must be a read capability")
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("targetKey", request.TargetKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("templateKey", request.TemplateKey)...)
|
||||
violations = appendRequired(violations, "adapterVersion", request.AdapterVersion)
|
||||
if request.AdapterVersion != "" && containsSCUMProtectedMaterial(request.AdapterVersion) {
|
||||
violations = append(violations, "adapterVersion contains protected material")
|
||||
}
|
||||
if request.AdapterVersion != "" && request.Binding.AdapterVersion != "" && request.AdapterVersion != request.Binding.AdapterVersion {
|
||||
violations = append(violations, "adapterVersion must match binding.adapterVersion")
|
||||
}
|
||||
if !validSCUMFingerprint(request.RequiredSchemaFingerprint) {
|
||||
violations = append(violations, "requiredSchemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
if !validSCUMDigest(request.AssetDigest) {
|
||||
violations = append(violations, "assetDigest must be sha256 digest")
|
||||
}
|
||||
if !validSCUMDigest(request.ParameterDigest) {
|
||||
violations = append(violations, "parameterDigest must be sha256 digest")
|
||||
}
|
||||
violations = append(violations, validateSCUMSQLiteTemplateBounds("bounds", request.Bounds)...)
|
||||
violations = append(violations, validateSCUMValueMap("parameters", request.Parameters, request.Bounds.MaxParameters)...)
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMTypedRCONTemplateRequest(request domain.SCUMTypedRCONTemplateRequest) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", request.RequestID)
|
||||
violations = appendRequired(violations, "jobId", request.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", request.Binding)...)
|
||||
if !validSCUMRCONWriteCapability(request.Capability) {
|
||||
violations = append(violations, "capability must be a typed RCON write capability")
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("transportKey", request.TransportKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("targetKey", request.TargetKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("templateKey", request.TemplateKey)...)
|
||||
violations = appendRequired(violations, "adapterVersion", request.AdapterVersion)
|
||||
if request.AdapterVersion != "" && containsSCUMProtectedMaterial(request.AdapterVersion) {
|
||||
violations = append(violations, "adapterVersion contains protected material")
|
||||
}
|
||||
if request.AdapterVersion != "" && request.Binding.AdapterVersion != "" && request.AdapterVersion != request.Binding.AdapterVersion {
|
||||
violations = append(violations, "adapterVersion must match binding.adapterVersion")
|
||||
}
|
||||
if request.RequiredSchemaFingerprint != "" && !validSCUMFingerprint(request.RequiredSchemaFingerprint) {
|
||||
violations = append(violations, "requiredSchemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
for _, item := range []struct{ name, value string }{{"assetDigest", request.AssetDigest}, {"payloadDigest", request.PayloadDigest}, {"confirmationDigest", request.ConfirmationDigest}, {"targetIdentityDigest", request.TargetIdentityDigest}} {
|
||||
if !validSCUMDigest(item.value) {
|
||||
violations = append(violations, item.name+" must be sha256 digest")
|
||||
}
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("idempotencyKey", request.IdempotencyKey)...)
|
||||
if strings.TrimSpace(request.ReviewReason) == "" || len(request.ReviewReason) > 320 || containsSCUMProtectedMaterial(request.ReviewReason) {
|
||||
violations = append(violations, "reviewReason is unsafe")
|
||||
}
|
||||
violations = append(violations, validateSCUMTypedRCONTemplateBounds("bounds", request.Bounds)...)
|
||||
violations = append(violations, validateSCUMValueMap("payload", request.Payload, 64)...)
|
||||
violations = append(violations, validateSCUMJSONSize("payload", request.Payload, request.Bounds.MaxPayloadBytes)...)
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMGuardedMutationRequest(request domain.SCUMGuardedMutationRequest) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", request.RequestID)
|
||||
violations = appendRequired(violations, "jobId", request.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", request.Binding)...)
|
||||
if !validSCUMGuardedMutationCapability(request.Capability) {
|
||||
violations = append(violations, "capability must be a guarded database/XML write capability")
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("targetKey", request.TargetKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("templateKey", request.TemplateKey)...)
|
||||
violations = appendRequired(violations, "adapterVersion", request.AdapterVersion)
|
||||
if request.AdapterVersion != "" && containsSCUMProtectedMaterial(request.AdapterVersion) {
|
||||
violations = append(violations, "adapterVersion contains protected material")
|
||||
}
|
||||
if request.AdapterVersion != "" && request.Binding.AdapterVersion != "" && request.AdapterVersion != request.Binding.AdapterVersion {
|
||||
violations = append(violations, "adapterVersion must match binding.adapterVersion")
|
||||
}
|
||||
if !validSCUMFingerprint(request.RequiredSchemaFingerprint) {
|
||||
violations = append(violations, "requiredSchemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
for _, item := range scumGuardedMutationRequestDigests(request) {
|
||||
if !validSCUMDigest(item.value) {
|
||||
violations = append(violations, item.name+" must be sha256 digest")
|
||||
}
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("idempotencyKey", request.IdempotencyKey)...)
|
||||
if strings.TrimSpace(request.ReviewReason) == "" || len(request.ReviewReason) > 320 || containsSCUMProtectedMaterial(request.ReviewReason) || containsSCUMRawXML(request.ReviewReason) {
|
||||
violations = append(violations, "reviewReason is unsafe")
|
||||
}
|
||||
violations = append(violations, validateSCUMGuardedMutationBounds("bounds", request.Bounds)...)
|
||||
violations = append(violations, validateSCUMGuardedMutationPayload("payload", request.Payload)...)
|
||||
violations = append(violations, validateSCUMJSONSize("payload", request.Payload, request.Bounds.MaxPayloadBytes)...)
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMSchemaProbeResult(result domain.SCUMSchemaProbeResult) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", result.RequestID)
|
||||
violations = appendRequired(violations, "jobId", result.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", result.Binding)...)
|
||||
if !validSCUMSchemaProbeResultStatus(result.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
if result.SourceFingerprint != "" && !validSCUMFingerprint(result.SourceFingerprint) {
|
||||
violations = append(violations, "sourceFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
if result.SchemaFingerprint != "" && !validSCUMFingerprint(result.SchemaFingerprint) {
|
||||
violations = append(violations, "schemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
if result.ResultDigest != "" && !validSCUMFingerprint(result.ResultDigest) {
|
||||
violations = append(violations, "resultDigest must be a digest/fingerprint")
|
||||
}
|
||||
violations = append(violations, validateSCUMSafeError("safeError", result.SafeError)...)
|
||||
violations = append(violations, validateSCUMSchemaProbeBounds("limits", result.Limits)...)
|
||||
if len(result.Objects) > result.Limits.MaxObjects && result.Limits.MaxObjects > 0 {
|
||||
violations = append(violations, "objects exceeds declared limit")
|
||||
}
|
||||
for i, object := range result.Objects {
|
||||
field := fmt.Sprintf("objects[%d]", i)
|
||||
violations = append(violations, validateSCUMSchemaObjectEvidence(field, object)...)
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMSQLiteTemplateResult(result domain.SCUMSQLiteTemplateResult) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", result.RequestID)
|
||||
violations = appendRequired(violations, "jobId", result.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", result.Binding)...)
|
||||
if !validSCUMTerminalResultStatus(result.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
if !validSCUMReadCapability(result.Capability) {
|
||||
violations = append(violations, "capability must be a read capability")
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("targetKey", result.TargetKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("templateKey", result.TemplateKey)...)
|
||||
violations = appendRequired(violations, "adapterVersion", result.AdapterVersion)
|
||||
if result.AdapterVersion != "" && containsSCUMProtectedMaterial(result.AdapterVersion) {
|
||||
violations = append(violations, "adapterVersion contains protected material")
|
||||
}
|
||||
if result.AdapterVersion != "" && result.Binding.AdapterVersion != "" && result.AdapterVersion != result.Binding.AdapterVersion {
|
||||
violations = append(violations, "adapterVersion must match binding.adapterVersion")
|
||||
}
|
||||
if result.SchemaFingerprint != "" && !validSCUMFingerprint(result.SchemaFingerprint) {
|
||||
violations = append(violations, "schemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
if !validSCUMDigest(result.AssetDigest) {
|
||||
violations = append(violations, "assetDigest must be sha256 digest")
|
||||
}
|
||||
if !validSCUMDigest(result.ParameterDigest) {
|
||||
violations = append(violations, "parameterDigest must be sha256 digest")
|
||||
}
|
||||
if result.SourceFingerprint != "" && !validSCUMFingerprint(result.SourceFingerprint) {
|
||||
violations = append(violations, "sourceFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
if !validSCUMFingerprint(result.ResultDigest) {
|
||||
violations = append(violations, "resultDigest must be a digest/fingerprint")
|
||||
}
|
||||
violations = append(violations, validateSCUMSafeError("safeError", result.SafeError)...)
|
||||
violations = append(violations, validateSCUMSQLiteTemplateBounds("limits", result.Limits)...)
|
||||
if result.RowCount != len(result.Rows) {
|
||||
violations = append(violations, "rowCount must match returned rows")
|
||||
}
|
||||
if len(result.Rows) > result.Limits.MaxRows && result.Limits.MaxRows > 0 {
|
||||
violations = append(violations, "rows exceeds declared limit")
|
||||
}
|
||||
for i, row := range result.Rows {
|
||||
violations = append(violations, validateSCUMValueMap(fmt.Sprintf("rows[%d]", i), row, 256)...)
|
||||
}
|
||||
if result.Status == domain.SCUMTerminalResultSucceeded {
|
||||
if result.SchemaFingerprint == "" || result.SourceFingerprint == "" {
|
||||
violations = append(violations, "succeeded result requires schema/source fingerprints")
|
||||
}
|
||||
if result.SafeError.Code != "" && result.SafeError.Code != domain.SCUMSafeErrorNone {
|
||||
violations = append(violations, "succeeded result must not carry an error code")
|
||||
}
|
||||
} else if result.SafeError.Code == "" || result.SafeError.Code == domain.SCUMSafeErrorNone {
|
||||
violations = append(violations, "non-succeeded result requires a safe error code")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMTypedRCONTemplateResult(result domain.SCUMTypedRCONTemplateResult) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", result.RequestID)
|
||||
violations = appendRequired(violations, "jobId", result.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", result.Binding)...)
|
||||
if !validSCUMTerminalResultStatus(result.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
if !validSCUMRCONWriteCapability(result.Capability) {
|
||||
violations = append(violations, "capability must be a typed RCON write capability")
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("transportKey", result.TransportKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("targetKey", result.TargetKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("templateKey", result.TemplateKey)...)
|
||||
violations = appendRequired(violations, "adapterVersion", result.AdapterVersion)
|
||||
if result.AdapterVersion != "" && containsSCUMProtectedMaterial(result.AdapterVersion) {
|
||||
violations = append(violations, "adapterVersion contains protected material")
|
||||
}
|
||||
if result.AdapterVersion != "" && result.Binding.AdapterVersion != "" && result.AdapterVersion != result.Binding.AdapterVersion {
|
||||
violations = append(violations, "adapterVersion must match binding.adapterVersion")
|
||||
}
|
||||
if result.SchemaFingerprint != "" && !validSCUMFingerprint(result.SchemaFingerprint) {
|
||||
violations = append(violations, "schemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
for _, item := range []struct{ name, value string }{{"assetDigest", result.AssetDigest}, {"payloadDigest", result.PayloadDigest}, {"confirmationDigest", result.ConfirmationDigest}, {"targetIdentityDigest", result.TargetIdentityDigest}, {"resultDigest", result.ResultDigest}} {
|
||||
if !validSCUMDigest(item.value) {
|
||||
violations = append(violations, item.name+" must be sha256 digest")
|
||||
}
|
||||
}
|
||||
if result.ResponseDigest != "" && !validSCUMDigest(result.ResponseDigest) {
|
||||
violations = append(violations, "responseDigest must be sha256 digest")
|
||||
}
|
||||
if result.ConfirmationDigestID != "" && !validSCUMDigest(result.ConfirmationDigestID) {
|
||||
violations = append(violations, "confirmationDigestId must be sha256 digest")
|
||||
}
|
||||
if !validSCUMRCONConfirmationStatus(result.ConfirmationStatus) {
|
||||
violations = append(violations, "confirmationStatus is invalid")
|
||||
}
|
||||
if len(result.SafeSummary) > 320 || containsSCUMProtectedMaterial(result.SafeSummary) {
|
||||
violations = append(violations, "safeSummary is unsafe")
|
||||
}
|
||||
violations = append(violations, validateSCUMSafeError("safeError", result.SafeError)...)
|
||||
violations = append(violations, validateSCUMTypedRCONTemplateBounds("limits", result.Limits)...)
|
||||
if result.Status == domain.SCUMTerminalResultSucceeded {
|
||||
if result.ConfirmationStatus != domain.SCUMRCONConfirmationConfirmed || result.ResponseDigest == "" || result.ConfirmationDigestID == "" {
|
||||
violations = append(violations, "succeeded result requires confirmed response and confirmation digests")
|
||||
}
|
||||
if result.SafeError.Code != "" && result.SafeError.Code != domain.SCUMSafeErrorNone {
|
||||
violations = append(violations, "succeeded result must not carry an error code")
|
||||
}
|
||||
} else if result.SafeError.Code == "" || result.SafeError.Code == domain.SCUMSafeErrorNone {
|
||||
violations = append(violations, "non-succeeded result requires a safe error code")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMGuardedMutationResult(result domain.SCUMGuardedMutationResult) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", result.RequestID)
|
||||
violations = appendRequired(violations, "jobId", result.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", result.Binding)...)
|
||||
if !validSCUMTerminalResultStatus(result.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
if !validSCUMGuardedMutationCapability(result.Capability) {
|
||||
violations = append(violations, "capability must be a guarded database/XML write capability")
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("targetKey", result.TargetKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("templateKey", result.TemplateKey)...)
|
||||
violations = appendRequired(violations, "adapterVersion", result.AdapterVersion)
|
||||
if result.AdapterVersion != "" && containsSCUMProtectedMaterial(result.AdapterVersion) {
|
||||
violations = append(violations, "adapterVersion contains protected material")
|
||||
}
|
||||
if result.AdapterVersion != "" && result.Binding.AdapterVersion != "" && result.AdapterVersion != result.Binding.AdapterVersion {
|
||||
violations = append(violations, "adapterVersion must match binding.adapterVersion")
|
||||
}
|
||||
if !validSCUMFingerprint(result.SchemaFingerprint) {
|
||||
violations = append(violations, "schemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
if result.SourceFingerprint != "" && !validSCUMFingerprint(result.SourceFingerprint) {
|
||||
violations = append(violations, "sourceFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
for _, item := range scumGuardedMutationResultDigests(result) {
|
||||
if !validSCUMDigest(item.value) {
|
||||
violations = append(violations, item.name+" must be sha256 digest")
|
||||
}
|
||||
}
|
||||
if !validSCUMMutationReadbackStatus(result.ReadbackStatus) {
|
||||
violations = append(violations, "readbackStatus is invalid")
|
||||
}
|
||||
if len(result.SafeSummary) > 320 || containsSCUMProtectedMaterial(result.SafeSummary) || containsSCUMRawXML(result.SafeSummary) {
|
||||
violations = append(violations, "safeSummary is unsafe")
|
||||
}
|
||||
violations = append(violations, validateSCUMSafeError("safeError", result.SafeError)...)
|
||||
violations = append(violations, validateSCUMGuardedMutationBounds("limits", result.Limits)...)
|
||||
if result.Status == domain.SCUMTerminalResultSucceeded {
|
||||
if result.AffectedRows != 1 || result.Limits.MaxAffectedRows != 1 {
|
||||
violations = append(violations, "succeeded result requires exactly one affected row")
|
||||
}
|
||||
if result.ReadbackStatus != domain.SCUMMutationReadbackConfirmed || result.SourceFingerprint == "" || result.BeforeDigest == "" || result.AfterDigest == "" || result.ReadbackDigest == "" {
|
||||
violations = append(violations, "succeeded result requires conclusive before/after/readback digests")
|
||||
}
|
||||
if result.SafeError.Code != "" && result.SafeError.Code != domain.SCUMSafeErrorNone {
|
||||
violations = append(violations, "succeeded result must not carry an error code")
|
||||
}
|
||||
} else if result.SafeError.Code == "" || result.SafeError.Code == domain.SCUMSafeErrorNone {
|
||||
violations = append(violations, "non-succeeded result requires a safe error code")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMParsedLogBatchResult(result domain.SCUMParsedLogBatchResult) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", result.RequestID)
|
||||
violations = appendRequired(violations, "jobId", result.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", result.Binding)...)
|
||||
if !validSCUMTerminalResultStatus(result.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
for _, item := range []struct{ name, value string }{{"sourceKey", result.SourceKey}, {"streamKey", result.StreamKey}, {"parserKey", result.ParserKey}, {"parserVersion", result.ParserVersion}, {"adapterVersion", result.AdapterVersion}} {
|
||||
violations = append(violations, validateSCUMTemplateKey(item.name, item.value)...)
|
||||
if containsSCUMProtectedMaterial(item.value) || containsSCUMNetworkMaterial(item.value) {
|
||||
violations = append(violations, item.name+" contains protected material")
|
||||
}
|
||||
}
|
||||
for _, item := range []struct{ name, value string }{{"assetDigest", result.AssetDigest}, {"parserDigest", result.ParserDigest}, {"resultDigest", result.ResultDigest}} {
|
||||
if !validSCUMDigest(item.value) {
|
||||
violations = append(violations, item.name+" must be sha256 digest")
|
||||
}
|
||||
}
|
||||
violations = append(violations, validateSCUMParsedLogCursor("firstCursor", result.FirstCursor)...)
|
||||
violations = append(violations, validateSCUMParsedLogCursor("lastCursor", result.LastCursor)...)
|
||||
if !validSCUMLogTailState(result.TailState) {
|
||||
violations = append(violations, "tailState is invalid")
|
||||
}
|
||||
if result.EventCount != len(result.Events) {
|
||||
violations = append(violations, "eventCount must match returned events")
|
||||
}
|
||||
violations = append(violations, validateSCUMParsedLogBatchBounds("limits", result.Limits)...)
|
||||
if len(result.Events) > result.Limits.MaxEvents && result.Limits.MaxEvents > 0 {
|
||||
violations = append(violations, "events exceeds declared limit")
|
||||
}
|
||||
if len(result.SafeSummary) > 320 || containsSCUMProtectedMaterial(result.SafeSummary) || containsSCUMNetworkMaterial(result.SafeSummary) || containsSCUMRawXML(result.SafeSummary) {
|
||||
violations = append(violations, "safeSummary is unsafe")
|
||||
}
|
||||
violations = append(violations, validateSCUMSafeError("safeError", result.SafeError)...)
|
||||
if containsSCUMNetworkMaterial(result.SafeError.Message) || containsSCUMRawXML(result.SafeError.Message) {
|
||||
violations = append(violations, "safeError.message is unsafe")
|
||||
}
|
||||
seenLogical := map[string]struct{}{}
|
||||
seenTransport := map[string]struct{}{}
|
||||
for i, event := range result.Events {
|
||||
field := fmt.Sprintf("events[%d]", i)
|
||||
violations = append(violations, validateSCUMParsedLogEvent(field, event, result)...)
|
||||
if event.LogicalEventDigest != "" {
|
||||
if _, exists := seenLogical[event.LogicalEventDigest]; exists {
|
||||
violations = append(violations, field+".logicalEventDigest is duplicated")
|
||||
}
|
||||
seenLogical[event.LogicalEventDigest] = struct{}{}
|
||||
}
|
||||
transport := fmt.Sprintf("%s|%s|%d", event.Cursor.SourceIdentityDigest, event.Cursor.StreamGeneration, event.Cursor.Sequence)
|
||||
if _, exists := seenTransport[transport]; exists {
|
||||
violations = append(violations, field+".cursor is duplicated")
|
||||
}
|
||||
seenTransport[transport] = struct{}{}
|
||||
}
|
||||
if result.EventCount > 0 && result.LastCursor.Sequence < result.FirstCursor.Sequence {
|
||||
violations = append(violations, "lastCursor.sequence must not be before firstCursor.sequence")
|
||||
}
|
||||
if result.Status == domain.SCUMTerminalResultSucceeded {
|
||||
if result.SafeError.Code != "" && result.SafeError.Code != domain.SCUMSafeErrorNone {
|
||||
violations = append(violations, "succeeded result must not carry an error code")
|
||||
}
|
||||
} else if result.SafeError.Code == "" || result.SafeError.Code == domain.SCUMSafeErrorNone {
|
||||
violations = append(violations, "non-succeeded result requires a safe error code")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func scumGuardedMutationRequestDigests(request domain.SCUMGuardedMutationRequest) []struct{ name, value string } {
|
||||
return []struct{ name, value string }{{"assetDigest", request.AssetDigest}, {"targetIdentityDigest", request.TargetIdentityDigest}, {"expectedRowDigest", request.ExpectedRowDigest}, {"expectedValueDigest", request.ExpectedValueDigest}, {"expectedXmlDigest", request.ExpectedXMLDigest}, {"patchDigest", request.PatchDigest}, {"backupEvidenceDigest", request.BackupEvidenceDigest}, {"offlineEvidenceDigest", request.OfflineEvidenceDigest}, {"dangerConfirmationDigest", request.DangerConfirmationDigest}, {"readbackExpectationDigest", request.ReadbackExpectationDigest}}
|
||||
}
|
||||
|
||||
func scumGuardedMutationResultDigests(result domain.SCUMGuardedMutationResult) []struct{ name, value string } {
|
||||
items := []struct{ name, value string }{{"assetDigest", result.AssetDigest}, {"targetIdentityDigest", result.TargetIdentityDigest}, {"expectedRowDigest", result.ExpectedRowDigest}, {"expectedValueDigest", result.ExpectedValueDigest}, {"expectedXmlDigest", result.ExpectedXMLDigest}, {"patchDigest", result.PatchDigest}, {"backupEvidenceDigest", result.BackupEvidenceDigest}, {"offlineEvidenceDigest", result.OfflineEvidenceDigest}, {"dangerConfirmationDigest", result.DangerConfirmationDigest}, {"readbackExpectationDigest", result.ReadbackExpectationDigest}, {"resultDigest", result.ResultDigest}}
|
||||
for _, item := range []struct{ name, value string }{{"beforeDigest", result.BeforeDigest}, {"afterDigest", result.AfterDigest}, {"readbackDigest", result.ReadbackDigest}} {
|
||||
if item.value != "" {
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func ValidateSCUMCapabilityEvidence(evidence domain.SCUMCapabilityEvidence) error {
|
||||
var violations []string
|
||||
if !validSCUMDataCapability(evidence.Capability) {
|
||||
violations = append(violations, "capability is invalid")
|
||||
}
|
||||
if !validSCUMCapabilityEvidenceStatus(evidence.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", evidence.Binding)...)
|
||||
violations = appendRequired(violations, "adapterVersion", evidence.AdapterVersion)
|
||||
if evidence.SchemaFingerprint != "" && !validSCUMFingerprint(evidence.SchemaFingerprint) {
|
||||
violations = append(violations, "schemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
if evidence.ProbeResultDigest != "" && !validSCUMFingerprint(evidence.ProbeResultDigest) {
|
||||
violations = append(violations, "probeResultDigest must be a digest/fingerprint")
|
||||
}
|
||||
for i, digest := range evidence.AssetDigests {
|
||||
if !validSCUMDigest(digest) {
|
||||
violations = append(violations, fmt.Sprintf("assetDigests[%d] must be sha256 digest", i))
|
||||
}
|
||||
}
|
||||
violations = append(violations, validateSCUMSafeError("safeError", evidence.SafeError)...)
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func validateSCUMLiveDataManifest(prefix string, value domain.SCUMLiveDataManifest, capabilities []string, remoteAccess domain.GamePluginRemoteAccess, runtimeProfiles domain.GamePluginRuntimeProfiles) []string {
|
||||
if value.SchemaVersion == "" && value.Probe.Capability == "" && len(value.CapabilityGates) == 0 {
|
||||
return nil
|
||||
}
|
||||
var violations []string
|
||||
if value.SchemaVersion != "1" {
|
||||
violations = append(violations, prefix+".schemaVersion must be 1")
|
||||
}
|
||||
if value.Probe.Capability != domain.JobCapabilityRemoteRunDBSQLiteProbe {
|
||||
violations = append(violations, prefix+".probe.capability must be "+domain.JobCapabilityRemoteRunDBSQLiteProbe)
|
||||
}
|
||||
if !containsString(capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) || !containsString(remoteAccess.RunCapabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) {
|
||||
violations = append(violations, prefix+".probe requires declared remote.run.db.sqlite.probe capability")
|
||||
}
|
||||
probeTransportFound := false
|
||||
probeDataTargetFound := false
|
||||
expectedWorkspaceKey := "databases/" + strings.TrimPrefix(value.Probe.TargetKey, "databases/")
|
||||
for _, transport := range runtimeProfiles.TransportProfiles {
|
||||
if transport.Key != value.Probe.TargetKey && transport.TargetKey != value.Probe.TargetKey {
|
||||
continue
|
||||
}
|
||||
probeTransportFound = true
|
||||
if transport.Kind != "sqlite" || !containsString(transport.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) {
|
||||
violations = append(violations, prefix+".probe.targetKey must reference sqlite transport with remote.run.db.sqlite.probe")
|
||||
}
|
||||
}
|
||||
if !probeTransportFound {
|
||||
violations = append(violations, prefix+".probe.targetKey must reference a declared transport")
|
||||
}
|
||||
for _, target := range runtimeProfiles.DataTargets {
|
||||
if target.Key != value.Probe.TargetKey {
|
||||
continue
|
||||
}
|
||||
probeDataTargetFound = true
|
||||
transport, ok := transportByKey(runtimeProfiles.TransportProfiles, target.TransportKey)
|
||||
if target.Kind != "sqlite.snapshot" || target.WorkspaceKey != expectedWorkspaceKey || !ok || transport.Kind != "sqlite" || !containsString(transport.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) {
|
||||
violations = append(violations, prefix+".probe.targetKey must reference a sqlite snapshot data target for the generated Run workspace")
|
||||
}
|
||||
}
|
||||
if !probeDataTargetFound {
|
||||
violations = append(violations, prefix+".probe.targetKey must reference a declared runtime data target")
|
||||
}
|
||||
violations = append(violations, validateSCUMSchemaProbeBounds(prefix+".probe.bounds", value.Probe.Bounds)...)
|
||||
seen := map[domain.SCUMDataCapability]struct{}{}
|
||||
for i, gate := range value.CapabilityGates {
|
||||
field := fmt.Sprintf("%s.capabilityGates[%d]", prefix, i)
|
||||
if !validSCUMDataCapability(gate.Capability) {
|
||||
violations = append(violations, field+".capability is invalid")
|
||||
}
|
||||
if _, exists := seen[gate.Capability]; exists {
|
||||
violations = append(violations, field+".capability is duplicated")
|
||||
}
|
||||
seen[gate.Capability] = struct{}{}
|
||||
if gate.Gate != domain.SCUMCapabilityGateDisabled && gate.Gate != domain.SCUMCapabilityGateEnabled {
|
||||
violations = append(violations, field+".gate is invalid")
|
||||
}
|
||||
violations = appendRequired(violations, field+".adapterVersion", gate.AdapterVersion)
|
||||
if !validSCUMCapabilityEvidenceStatus(gate.EvidenceStatus) {
|
||||
violations = append(violations, field+".evidenceStatus is invalid")
|
||||
}
|
||||
if containsSCUMProtectedMaterial(gate.SafeReason) || len(gate.SafeReason) > 240 || strings.TrimSpace(gate.SafeReason) == "" {
|
||||
violations = append(violations, field+".safeReason is unsafe")
|
||||
}
|
||||
if gate.Gate == domain.SCUMCapabilityGateEnabled {
|
||||
if gate.EvidenceStatus != domain.SCUMCapabilityEvidenceCompatible {
|
||||
violations = append(violations, field+".evidenceStatus must be compatible when enabled")
|
||||
}
|
||||
if !validSCUMFingerprint(gate.RequiredSchemaFingerprint) {
|
||||
violations = append(violations, field+".requiredSchemaFingerprint is required when enabled")
|
||||
}
|
||||
if gate.Capability != domain.SCUMDataCapabilitySchemaProbe && len(gate.RequiredAssetDigests) == 0 {
|
||||
violations = append(violations, field+".requiredAssetDigests is required when enabled")
|
||||
}
|
||||
}
|
||||
if gate.Gate == domain.SCUMCapabilityGateDisabled && gate.EvidenceStatus == domain.SCUMCapabilityEvidenceCompatible {
|
||||
violations = append(violations, field+".evidenceStatus must not claim compatibility while disabled")
|
||||
}
|
||||
for digestIndex, digest := range gate.RequiredAssetDigests {
|
||||
if !validSCUMDigest(digest) {
|
||||
violations = append(violations, fmt.Sprintf("%s.requiredAssetDigests[%d] must be sha256 digest", field, digestIndex))
|
||||
}
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMBindingIdentity(prefix string, value domain.SCUMBindingIdentity) []string {
|
||||
var violations []string
|
||||
for _, item := range []struct{ name, value string }{{"serverInstanceId", value.ServerInstanceID}, {"runBindingId", value.RunBindingID}, {"runEndpointId", value.RunEndpointID}, {"pluginId", value.PluginID}, {"pluginVersion", value.PluginVersion}, {"adapterVersion", value.AdapterVersion}, {"databaseIdentity", value.DatabaseIdentity}} {
|
||||
violations = appendRequired(violations, prefix+"."+item.name, item.value)
|
||||
if containsSCUMProtectedMaterial(item.value) {
|
||||
violations = append(violations, prefix+"."+item.name+" contains protected connection material")
|
||||
}
|
||||
}
|
||||
if value.GameVersion != "" && containsSCUMProtectedMaterial(value.GameVersion) {
|
||||
violations = append(violations, prefix+".gameVersion contains protected connection material")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMSchemaProbeBounds(prefix string, value domain.SCUMSchemaProbeBounds) []string {
|
||||
var violations []string
|
||||
if value.MaxObjects < 1 || value.MaxObjects > maxSCUMProbeObjects {
|
||||
violations = append(violations, prefix+".maxObjects is out of bounds")
|
||||
}
|
||||
if value.MaxColumnsPerObject < 1 || value.MaxColumnsPerObject > maxSCUMProbeColumnsPerObject {
|
||||
violations = append(violations, prefix+".maxColumnsPerObject is out of bounds")
|
||||
}
|
||||
if value.MaxIndexesPerObject < 0 || value.MaxIndexesPerObject > maxSCUMProbeIndexesPerObject {
|
||||
violations = append(violations, prefix+".maxIndexesPerObject is out of bounds")
|
||||
}
|
||||
if value.MaxForeignKeys < 0 || value.MaxForeignKeys > maxSCUMProbeForeignKeys {
|
||||
violations = append(violations, prefix+".maxForeignKeys is out of bounds")
|
||||
}
|
||||
if value.MaxCardinalityReads < 0 || value.MaxCardinalityReads > maxSCUMProbeObjects {
|
||||
violations = append(violations, prefix+".maxCardinalityReads is out of bounds")
|
||||
}
|
||||
if value.MaxSampleRows < 0 || value.MaxSampleRows > maxSCUMProbeSamples {
|
||||
violations = append(violations, prefix+".maxSampleRows is out of bounds")
|
||||
}
|
||||
if value.TimeoutMS < 1 || value.TimeoutMS > maxSCUMProbeTimeoutMS {
|
||||
violations = append(violations, prefix+".timeoutMs is out of bounds")
|
||||
}
|
||||
if value.MaxResultBytes < 1 || value.MaxResultBytes > maxSCUMProbeResultBytes {
|
||||
violations = append(violations, prefix+".maxResultBytes is out of bounds")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMSQLiteTemplateBounds(prefix string, value domain.SCUMSQLiteTemplateBounds) []string {
|
||||
var violations []string
|
||||
if value.MaxParameters < 0 || value.MaxParameters > maxSCUMTemplateParameters {
|
||||
violations = append(violations, prefix+".maxParameters is out of bounds")
|
||||
}
|
||||
if value.MaxRows < 1 || value.MaxRows > maxSCUMTemplateRows {
|
||||
violations = append(violations, prefix+".maxRows is out of bounds")
|
||||
}
|
||||
if value.TimeoutMS < 1 || value.TimeoutMS > maxSCUMProbeTimeoutMS {
|
||||
violations = append(violations, prefix+".timeoutMs is out of bounds")
|
||||
}
|
||||
if value.BusyTimeoutMS < 0 || value.BusyTimeoutMS > maxSCUMTemplateBusyTimeoutMS {
|
||||
violations = append(violations, prefix+".busyTimeoutMs is out of bounds")
|
||||
}
|
||||
if value.MaxResultBytes < 1 || value.MaxResultBytes > maxSCUMProbeResultBytes {
|
||||
violations = append(violations, prefix+".maxResultBytes is out of bounds")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMTypedRCONTemplateBounds(prefix string, value domain.SCUMTypedRCONTemplateBounds) []string {
|
||||
var violations []string
|
||||
if value.MaxPayloadBytes < 1 || value.MaxPayloadBytes > maxSCUMRCONPayloadBytes {
|
||||
violations = append(violations, prefix+".maxPayloadBytes is out of bounds")
|
||||
}
|
||||
if value.TimeoutMS < 1 || value.TimeoutMS > maxSCUMProbeTimeoutMS {
|
||||
violations = append(violations, prefix+".timeoutMs is out of bounds")
|
||||
}
|
||||
if value.MaxResponseBytes < 1 || value.MaxResponseBytes > maxSCUMRCONResponseBytes {
|
||||
violations = append(violations, prefix+".maxResponseBytes is out of bounds")
|
||||
}
|
||||
if value.MaxConfirmRecords < 1 || value.MaxConfirmRecords > maxSCUMRCONConfirmRecords {
|
||||
violations = append(violations, prefix+".maxConfirmRecords is out of bounds")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMGuardedMutationBounds(prefix string, value domain.SCUMGuardedMutationBounds) []string {
|
||||
var violations []string
|
||||
if value.MaxPayloadBytes < 1 || value.MaxPayloadBytes > maxSCUMMutationPayloadBytes {
|
||||
violations = append(violations, prefix+".maxPayloadBytes is out of bounds")
|
||||
}
|
||||
if value.TimeoutMS < 1 || value.TimeoutMS > maxSCUMProbeTimeoutMS {
|
||||
violations = append(violations, prefix+".timeoutMs is out of bounds")
|
||||
}
|
||||
if value.BusyTimeoutMS < 0 || value.BusyTimeoutMS > maxSCUMTemplateBusyTimeoutMS {
|
||||
violations = append(violations, prefix+".busyTimeoutMs is out of bounds")
|
||||
}
|
||||
if value.MaxReadbackBytes < 1 || value.MaxReadbackBytes > maxSCUMMutationReadbackBytes {
|
||||
violations = append(violations, prefix+".maxReadbackBytes is out of bounds")
|
||||
}
|
||||
if value.MaxAffectedRows != 1 {
|
||||
violations = append(violations, prefix+".maxAffectedRows must be 1")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMParsedLogBatchBounds(prefix string, value domain.SCUMParsedLogBatchBounds) []string {
|
||||
var violations []string
|
||||
if value.MaxEvents < 0 || value.MaxEvents > maxSCUMParsedLogEvents {
|
||||
violations = append(violations, prefix+".maxEvents is out of bounds")
|
||||
}
|
||||
if value.MaxPayloadBytes < 1 || value.MaxPayloadBytes > maxSCUMParsedLogPayloadBytes {
|
||||
violations = append(violations, prefix+".maxPayloadBytes is out of bounds")
|
||||
}
|
||||
if value.MaxLineBytes < 1 || value.MaxLineBytes > maxSCUMParsedLogLineBytes {
|
||||
violations = append(violations, prefix+".maxLineBytes is out of bounds")
|
||||
}
|
||||
if value.MaxResultBytes < 1 || value.MaxResultBytes > maxSCUMParsedLogResultBytes {
|
||||
violations = append(violations, prefix+".maxResultBytes is out of bounds")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMParsedLogCursor(prefix string, value domain.SCUMParsedLogCursor) []string {
|
||||
var violations []string
|
||||
if !validSCUMFingerprint(value.SourceIdentityDigest) || containsSCUMProtectedMaterial(value.SourceIdentityDigest) || containsSCUMNetworkMaterial(value.SourceIdentityDigest) {
|
||||
violations = append(violations, prefix+".sourceIdentityDigest must be a redacted fingerprint")
|
||||
}
|
||||
if !validSCUMFingerprint(value.StreamGeneration) || containsSCUMProtectedMaterial(value.StreamGeneration) || containsSCUMNetworkMaterial(value.StreamGeneration) {
|
||||
violations = append(violations, prefix+".streamGeneration must be a redacted fingerprint")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMParsedLogEvent(prefix string, event domain.SCUMParsedLogEvent, result domain.SCUMParsedLogBatchResult) []string {
|
||||
var violations []string
|
||||
violations = append(violations, validateSCUMTemplateKey(prefix+".eventType", event.EventType)...)
|
||||
violations = append(violations, validateSCUMParsedLogCursor(prefix+".cursor", event.Cursor)...)
|
||||
for _, item := range []struct{ name, value string }{{"logicalEventDigest", event.LogicalEventDigest}, {"eventDigest", event.EventDigest}, {"payloadDigest", event.PayloadDigest}} {
|
||||
if !validSCUMDigest(item.value) {
|
||||
violations = append(violations, prefix+"."+item.name+" must be sha256 digest")
|
||||
}
|
||||
}
|
||||
if result.EventCount > 0 && event.Cursor.Sequence < result.FirstCursor.Sequence || result.EventCount > 0 && event.Cursor.Sequence > result.LastCursor.Sequence {
|
||||
violations = append(violations, prefix+".cursor.sequence is outside batch range")
|
||||
}
|
||||
if event.Cursor.SourceIdentityDigest != result.FirstCursor.SourceIdentityDigest || event.Cursor.StreamGeneration != result.FirstCursor.StreamGeneration {
|
||||
violations = append(violations, prefix+".cursor must match batch source identity and generation")
|
||||
}
|
||||
violations = append(violations, validateSCUMValueMap(prefix+".payload", event.Payload, 64)...)
|
||||
violations = append(violations, validateSCUMJSONSize(prefix+".payload", event.Payload, result.Limits.MaxPayloadBytes)...)
|
||||
for key, value := range event.Payload {
|
||||
if strings.Contains(strings.ToLower(key), "raw") || containsSCUMProtectedMaterial(key) || containsSCUMNetworkMaterial(key) {
|
||||
violations = append(violations, prefix+".payload key is unsafe")
|
||||
}
|
||||
if text, ok := value.(string); ok && (containsSCUMProtectedMaterial(text) || containsSCUMNetworkMaterial(text) || containsSCUMRawXML(text)) {
|
||||
violations = append(violations, prefix+".payload value is unsafe")
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMTemplateKey(prefix, value string) []string {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, prefix, value)
|
||||
if value != "" && (!scumTemplateKeyPattern.MatchString(value) || strings.Contains(value, "..")) {
|
||||
violations = append(violations, prefix+" is unsafe")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMValueMap(prefix string, values map[string]any, maxItems int) []string {
|
||||
var violations []string
|
||||
if maxItems >= 0 && len(values) > maxItems {
|
||||
violations = append(violations, prefix+" exceeds declared limit")
|
||||
}
|
||||
for key, value := range values {
|
||||
loweredKey := strings.ToLower(key)
|
||||
if !scumTemplateKeyPattern.MatchString(key) || containsSCUMProtectedMaterial(key) || strings.Contains(key, "..") || strings.Contains(loweredKey, "command") || strings.Contains(loweredKey, "rcon") {
|
||||
violations = append(violations, prefix+" key is unsafe")
|
||||
}
|
||||
field := prefix + ".value"
|
||||
violations = append(violations, validateSCUMValue(field, value)...)
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMGuardedMutationPayload(prefix string, values map[string]any) []string {
|
||||
violations := validateSCUMValueMap(prefix, values, 64)
|
||||
for key, value := range values {
|
||||
loweredKey := strings.ToLower(key)
|
||||
if strings.Contains(loweredKey, "sql") || strings.Contains(loweredKey, "xml") || strings.Contains(loweredKey, "path") || strings.Contains(loweredKey, "table") || strings.Contains(loweredKey, "column") || strings.Contains(loweredKey, "query") || strings.Contains(loweredKey, "raw") || strings.Contains(loweredKey, "855") {
|
||||
violations = append(violations, prefix+" key is unsafe")
|
||||
}
|
||||
if text, ok := value.(string); ok {
|
||||
trimmed := strings.TrimSpace(strings.ToLower(text))
|
||||
if containsSCUMRawXML(text) || trimmed == "855" || strings.Contains(trimmed, "fieldkey=855") || strings.Contains(trimmed, "prisoner.value") {
|
||||
violations = append(violations, prefix+" value is unsafe")
|
||||
}
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMValue(prefix string, value any) []string {
|
||||
var violations []string
|
||||
switch item := value.(type) {
|
||||
case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
||||
return nil
|
||||
case string:
|
||||
if len([]byte(item)) > maxSCUMTemplateValueBytes {
|
||||
violations = append(violations, prefix+" is too large")
|
||||
}
|
||||
if containsSCUMProtectedMaterial(item) {
|
||||
violations = append(violations, prefix+" contains protected material")
|
||||
}
|
||||
default:
|
||||
violations = append(violations, prefix+" must be a scalar value")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMJSONSize(prefix string, value any, maxBytes int) []string {
|
||||
if maxBytes <= 0 {
|
||||
return []string{prefix + " max byte limit is required"}
|
||||
}
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return []string{prefix + " must be JSON serializable"}
|
||||
}
|
||||
if len(payload) > maxBytes {
|
||||
return []string{prefix + " exceeds declared byte limit"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSCUMSchemaObjectEvidence(prefix string, value domain.SCUMSchemaObjectEvidence) []string {
|
||||
var violations []string
|
||||
if !validSCUMFingerprint(value.ObjectHash) {
|
||||
violations = append(violations, prefix+".objectHash must be a digest/fingerprint")
|
||||
}
|
||||
if value.Kind != "table" && value.Kind != "view" && value.Kind != "index" && value.Kind != "trigger" {
|
||||
violations = append(violations, prefix+".kind is invalid")
|
||||
}
|
||||
if !validSCUMFingerprint(value.NameFingerprint) || containsSCUMProtectedMaterial(value.NameFingerprint) {
|
||||
violations = append(violations, prefix+".nameFingerprint must be redacted")
|
||||
}
|
||||
for i, column := range value.DeclaredColumns {
|
||||
field := fmt.Sprintf("%s.declaredColumns[%d]", prefix, i)
|
||||
if !validSCUMFingerprint(column.NameFingerprint) || containsSCUMProtectedMaterial(column.NameFingerprint) {
|
||||
violations = append(violations, field+".nameFingerprint must be redacted")
|
||||
}
|
||||
if containsSCUMProtectedMaterial(column.DeclaredType) {
|
||||
violations = append(violations, field+".declaredType contains protected material")
|
||||
}
|
||||
}
|
||||
for i, index := range value.Indexes {
|
||||
field := fmt.Sprintf("%s.indexes[%d]", prefix, i)
|
||||
if !validSCUMFingerprint(index.NameFingerprint) || containsSCUMProtectedMaterial(index.NameFingerprint) {
|
||||
violations = append(violations, field+".nameFingerprint must be redacted")
|
||||
}
|
||||
for columnIndex, hash := range index.ColumnHashes {
|
||||
if !validSCUMFingerprint(hash) {
|
||||
violations = append(violations, fmt.Sprintf("%s.columnHashes[%d] must be a digest/fingerprint", field, columnIndex))
|
||||
}
|
||||
}
|
||||
}
|
||||
for i, fk := range value.ForeignKeys {
|
||||
field := fmt.Sprintf("%s.foreignKeys[%d]", prefix, i)
|
||||
for _, item := range []struct{ name, value string }{{"fromColumnHash", fk.FromColumnHash}, {"toObjectHash", fk.ToObjectHash}, {"toColumnHash", fk.ToColumnHash}} {
|
||||
if !validSCUMFingerprint(item.value) {
|
||||
violations = append(violations, field+"."+item.name+" must be a digest/fingerprint")
|
||||
}
|
||||
}
|
||||
}
|
||||
for i, sample := range value.SampleFingerprints {
|
||||
if !validSCUMFingerprint(sample) || containsSCUMProtectedMaterial(sample) {
|
||||
violations = append(violations, fmt.Sprintf("%s.sampleFingerprints[%d] must be a redacted fingerprint", prefix, i))
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMSafeError(prefix string, value domain.SCUMSafeError) []string {
|
||||
var violations []string
|
||||
if !validSCUMSafeErrorCode(value.Code) {
|
||||
violations = append(violations, prefix+".code is invalid")
|
||||
}
|
||||
if containsSCUMProtectedMaterial(value.Message) {
|
||||
violations = append(violations, prefix+".message contains protected material")
|
||||
}
|
||||
if len(value.Message) > 320 {
|
||||
violations = append(violations, prefix+".message is too long")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validSCUMDataCapability(value domain.SCUMDataCapability) bool {
|
||||
switch value {
|
||||
case domain.SCUMDataCapabilitySchemaProbe, domain.SCUMDataCapabilityPlayerRead, domain.SCUMDataCapabilityPlayerDetailRead, domain.SCUMDataCapabilitySquadRead, domain.SCUMDataCapabilitySquadMemberRead, domain.SCUMDataCapabilityVehicleRead, domain.SCUMDataCapabilityFlagRead, domain.SCUMDataCapabilityPositionRead, domain.SCUMDataCapabilityProfileXMLWrite, domain.SCUMDataCapabilityEconomyCommand, domain.SCUMDataCapabilityGiftCommand:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMReadCapability(value domain.SCUMDataCapability) bool {
|
||||
switch value {
|
||||
case domain.SCUMDataCapabilityPlayerRead, domain.SCUMDataCapabilityPlayerDetailRead, domain.SCUMDataCapabilitySquadRead, domain.SCUMDataCapabilitySquadMemberRead, domain.SCUMDataCapabilityVehicleRead, domain.SCUMDataCapabilityFlagRead, domain.SCUMDataCapabilityPositionRead:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMRCONWriteCapability(value domain.SCUMDataCapability) bool {
|
||||
switch value {
|
||||
case domain.SCUMDataCapabilityEconomyCommand, domain.SCUMDataCapabilityGiftCommand:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMGuardedMutationCapability(value domain.SCUMDataCapability) bool {
|
||||
switch value {
|
||||
case domain.SCUMDataCapabilityProfileXMLWrite:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMTerminalResultStatus(value domain.SCUMTerminalResultStatus) bool {
|
||||
switch value {
|
||||
case domain.SCUMTerminalResultSucceeded, domain.SCUMTerminalResultFailed, domain.SCUMTerminalResultCancelled:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMRCONConfirmationStatus(value domain.SCUMRCONConfirmationStatus) bool {
|
||||
switch value {
|
||||
case domain.SCUMRCONConfirmationConfirmed, domain.SCUMRCONConfirmationFailed, domain.SCUMRCONConfirmationUnknown:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMMutationReadbackStatus(value domain.SCUMMutationReadbackStatus) bool {
|
||||
switch value {
|
||||
case domain.SCUMMutationReadbackConfirmed, domain.SCUMMutationReadbackFailed, domain.SCUMMutationReadbackConflict, domain.SCUMMutationReadbackUnknown:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMLogTailState(value domain.SCUMLogTailState) bool {
|
||||
switch value {
|
||||
case domain.SCUMLogTailAdvanced, domain.SCUMLogTailRotated, domain.SCUMLogTailTruncated, domain.SCUMLogTailRestarted, domain.SCUMLogTailPartial, domain.SCUMLogTailReplayed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMCapabilityEvidenceStatus(value domain.SCUMCapabilityEvidenceStatus) bool {
|
||||
switch value {
|
||||
case domain.SCUMCapabilityEvidenceMissing, domain.SCUMCapabilityEvidenceCompatible, domain.SCUMCapabilityEvidenceIncompatible, domain.SCUMCapabilityEvidenceFailed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMSchemaProbeResultStatus(value domain.SCUMCapabilityEvidenceStatus) bool {
|
||||
switch value {
|
||||
case domain.SCUMSchemaProbeStatusSucceeded, domain.SCUMCapabilityEvidenceCompatible, domain.SCUMCapabilityEvidenceFailed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMSafeErrorCode(value domain.SCUMSafeErrorCode) bool {
|
||||
switch value {
|
||||
case "", domain.SCUMSafeErrorNone, domain.SCUMSafeErrorProbeExecutorAbsent, domain.SCUMSafeErrorProbeMissing, domain.SCUMSafeErrorProbeFailed, domain.SCUMSafeErrorSchemaIncompatible, domain.SCUMSafeErrorBindingMismatch, domain.SCUMSafeErrorAdapterMismatch, domain.SCUMSafeErrorFingerprintMismatch, domain.SCUMSafeErrorDigestMismatch, domain.SCUMSafeErrorEvidenceExpired, domain.SCUMSafeErrorInvalidProbePayload, domain.SCUMSafeErrorInvalidRequest, domain.SCUMSafeErrorTargetUnavailable, domain.SCUMSafeErrorSourceUnavailable, domain.SCUMSafeErrorSQLiteOpenFailed, domain.SCUMSafeErrorSQLiteReadFailed, domain.SCUMSafeErrorDatabaseBusy, domain.SCUMSafeErrorTimeout, domain.SCUMSafeErrorCancelled, domain.SCUMSafeErrorSourceChanged, domain.SCUMSafeErrorResultLimitExceeded, domain.SCUMSafeErrorTemplateMissing, domain.SCUMSafeErrorTemplateMismatch, domain.SCUMSafeErrorParameterInvalid, domain.SCUMSafeErrorRowLimitExceeded, domain.SCUMSafeErrorResultSchemaInvalid, domain.SCUMSafeErrorMutationGuardMismatch, domain.SCUMSafeErrorMutationBackupUnavailable, domain.SCUMSafeErrorMutationOfflineRequired, domain.SCUMSafeErrorMutationConfirmationMissing, domain.SCUMSafeErrorMutationPatchInvalid, domain.SCUMSafeErrorAffectedRowsMismatch, domain.SCUMSafeErrorReadbackMismatch, domain.SCUMSafeErrorRollbackFailed:
|
||||
return true
|
||||
default:
|
||||
return scumDataTargetSafeErrorPattern.MatchString(string(value))
|
||||
}
|
||||
}
|
||||
|
||||
func containsSCUMRawXML(value string) bool {
|
||||
return regexp.MustCompile(`<\s*/?\s*[A-Za-z][^>]*>`).MatchString(value)
|
||||
}
|
||||
|
||||
func validSCUMFingerprint(value string) bool { return scumHashPattern.MatchString(value) }
|
||||
func validSCUMDigest(value string) bool {
|
||||
return regexp.MustCompile(`^sha256:[a-fA-F0-9]{64}$`).MatchString(value)
|
||||
}
|
||||
|
||||
func containsSCUMProtectedMaterial(value string) bool {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
lowered := strings.ToLower(trimmed)
|
||||
if trimmed == "" {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(lowered, "select ") || strings.Contains(lowered, "insert into") || strings.Contains(lowered, "update ") || strings.Contains(lowered, "delete from") || strings.Contains(lowered, "pragma ") || strings.Contains(lowered, "attach database") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(lowered, "dsn") || strings.Contains(lowered, "password") || strings.Contains(lowered, "credential") || strings.Contains(lowered, "token") || strings.Contains(lowered, "socket") || strings.Contains(lowered, "rcon") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(lowered, "sqlite://") || strings.HasPrefix(lowered, "mysql://") || strings.HasPrefix(lowered, "file://") || strings.HasPrefix(lowered, "tcp://") || strings.HasPrefix(lowered, "unix://") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "/") || strings.HasPrefix(trimmed, "\\\\") || regexp.MustCompile(`[A-Za-z]:[\\/]`).MatchString(trimmed) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsSCUMNetworkMaterial(value string) bool {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
lowered := strings.ToLower(trimmed)
|
||||
if trimmed == "" {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(lowered, "ip=") || strings.Contains(lowered, "addr=") || strings.Contains(lowered, "endpoint=") || strings.Contains(lowered, "port=") {
|
||||
return true
|
||||
}
|
||||
if regexp.MustCompile(`\b(?:\d{1,3}\.){3}\d{1,3}\b`).MatchString(trimmed) {
|
||||
return true
|
||||
}
|
||||
for _, token := range regexp.MustCompile(`[\s,;()\[\]{}'"]+`).Split(trimmed, -1) {
|
||||
if strings.Contains(token, ":") && net.ParseIP(strings.Trim(token, "<>")) != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
const scumProbeHash = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
|
||||
func TestValidateSCUMSchemaProbeRequestAllowsBoundedGenericProbe(t *testing.T) {
|
||||
request := domain.SCUMSchemaProbeRequest{RequestID: "probe-1", JobID: "job-1", Binding: validatorSCUMBinding(), Bounds: domain.DefaultSCUMSchemaProbeBounds(), RequestedAt: time.Now()}
|
||||
|
||||
if err := ValidateSCUMSchemaProbeRequest(request); err != nil {
|
||||
t.Fatalf("expected valid probe request, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMSchemaProbeRequestRejectsHostPathsAndLooseBounds(t *testing.T) {
|
||||
request := domain.SCUMSchemaProbeRequest{RequestID: "probe-1", JobID: "job-1", Binding: validatorSCUMBinding(), Bounds: domain.DefaultSCUMSchemaProbeBounds()}
|
||||
request.Binding.DatabaseIdentity = `C:\SCUM\Saved\SaveFiles\SCUM.db`
|
||||
request.Bounds.MaxSampleRows = 25
|
||||
|
||||
err := ValidateSCUMSchemaProbeRequest(request)
|
||||
if err == nil || !strings.Contains(err.Error(), "protected connection material") || !strings.Contains(err.Error(), "maxSampleRows") {
|
||||
t.Fatalf("expected protected material and bound violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMSchemaProbeResultRejectsRawSQLAndRows(t *testing.T) {
|
||||
result := domain.SCUMSchemaProbeResult{
|
||||
RequestID: "probe-1",
|
||||
JobID: "job-1",
|
||||
Binding: validatorSCUMBinding(),
|
||||
Status: domain.SCUMCapabilityEvidenceCompatible,
|
||||
SchemaFingerprint: scumProbeHash,
|
||||
ObservedAt: time.Now(),
|
||||
ResultDigest: scumProbeHash,
|
||||
Limits: domain.DefaultSCUMSchemaProbeBounds(),
|
||||
Objects: []domain.SCUMSchemaObjectEvidence{{
|
||||
ObjectHash: scumProbeHash,
|
||||
Kind: "table",
|
||||
NameFingerprint: "select * from players",
|
||||
DeclaredColumns: []domain.SCUMSchemaColumnEvidence{{NameFingerprint: scumProbeHash, DeclaredType: "TEXT", Ordinal: 1}},
|
||||
SampleFingerprints: []string{"{\"raw\":\"row\"}"},
|
||||
}},
|
||||
}
|
||||
|
||||
err := ValidateSCUMSchemaProbeResult(result)
|
||||
if err == nil || !strings.Contains(err.Error(), "nameFingerprint must be redacted") || !strings.Contains(err.Error(), "sampleFingerprints") {
|
||||
t.Fatalf("expected redaction violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMSchemaProbeResultAcceptsRunTerminalStatuses(t *testing.T) {
|
||||
succeeded := domain.SCUMSchemaProbeResult{RequestID: "probe-1", JobID: "job-1", Binding: validatorSCUMBinding(), Status: domain.SCUMSchemaProbeStatusSucceeded, SourceFingerprint: scumProbeHash, SchemaFingerprint: scumProbeHash, ObservedAt: time.Now(), ResultDigest: scumProbeHash, Limits: domain.DefaultSCUMSchemaProbeBounds(), SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||
if err := ValidateSCUMSchemaProbeResult(succeeded); err != nil {
|
||||
t.Fatalf("expected succeeded Run probe result to validate, got %v", err)
|
||||
}
|
||||
|
||||
succeeded.SourceFingerprint = "C:/db/SCUM.db"
|
||||
if err := ValidateSCUMSchemaProbeResult(succeeded); err == nil || !strings.Contains(err.Error(), "sourceFingerprint must be a digest/fingerprint") {
|
||||
t.Fatalf("expected raw source fingerprint rejection, got %v", err)
|
||||
}
|
||||
succeeded.SourceFingerprint = scumProbeHash
|
||||
|
||||
failed := succeeded
|
||||
failed.Status = domain.SCUMCapabilityEvidenceFailed
|
||||
failed.SchemaFingerprint = ""
|
||||
failed.SafeError = domain.SCUMSafeError{Code: domain.SCUMSafeErrorTargetUnavailable, Retryable: false}
|
||||
if err := ValidateSCUMSchemaProbeResult(failed); err != nil {
|
||||
t.Fatalf("expected safe failed Run probe result to validate, got %v", err)
|
||||
}
|
||||
|
||||
failed.SafeError = domain.SCUMSafeError{Code: domain.SCUMSafeErrorCode("data_target_plan_invalid"), Retryable: false}
|
||||
if err := ValidateSCUMSchemaProbeResult(failed); err != nil {
|
||||
t.Fatalf("expected generic Run data-target failure to validate, got %v", err)
|
||||
}
|
||||
|
||||
failed.SafeError = domain.SCUMSafeError{Code: domain.SCUMSafeErrorCode("data_target_invalid path"), Retryable: false}
|
||||
if err := ValidateSCUMSchemaProbeResult(failed); err == nil || !strings.Contains(err.Error(), "safeError.code is invalid") {
|
||||
t.Fatalf("expected unsafe data-target code rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMSQLiteTemplateRequestAllowsBoundedGenericTemplate(t *testing.T) {
|
||||
request := validatorSCUMSQLiteTemplateRequest()
|
||||
|
||||
if err := ValidateSCUMSQLiteTemplateRequest(request); err != nil {
|
||||
t.Fatalf("expected valid SQLite template request, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMSQLiteTemplateRequestRejectsSQLPathsAndLooseBounds(t *testing.T) {
|
||||
request := validatorSCUMSQLiteTemplateRequest()
|
||||
request.TemplateKey = "select * from players"
|
||||
request.Parameters = map[string]any{"profilePath": `C:\SCUM\Saved\SCUM.db`}
|
||||
request.Bounds.MaxRows = 50000
|
||||
|
||||
err := ValidateSCUMSQLiteTemplateRequest(request)
|
||||
if err == nil || !strings.Contains(err.Error(), "templateKey is unsafe") || !strings.Contains(err.Error(), "protected material") || !strings.Contains(err.Error(), "maxRows") {
|
||||
t.Fatalf("expected template/key/bounds violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMSQLiteTemplateResultAcceptsTypedRows(t *testing.T) {
|
||||
result := validatorSCUMSQLiteTemplateResult()
|
||||
|
||||
if err := ValidateSCUMSQLiteTemplateResult(result); err != nil {
|
||||
t.Fatalf("expected valid SQLite template result, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMSQLiteTemplateResultRejectsUnsafeRowsAndMismatchedCounts(t *testing.T) {
|
||||
result := validatorSCUMSQLiteTemplateResult()
|
||||
result.RowCount = 2
|
||||
result.Rows[0]["displayName"] = "select * from user_profile"
|
||||
|
||||
err := ValidateSCUMSQLiteTemplateResult(result)
|
||||
if err == nil || !strings.Contains(err.Error(), "rowCount") || !strings.Contains(err.Error(), "protected material") {
|
||||
t.Fatalf("expected row-count and protected-row violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMTypedRCONTemplateRequestAllowsBoundedGenericTemplate(t *testing.T) {
|
||||
request := validatorSCUMTypedRCONTemplateRequest()
|
||||
|
||||
if err := ValidateSCUMTypedRCONTemplateRequest(request); err != nil {
|
||||
t.Fatalf("expected valid typed RCON template request, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMTypedRCONTemplateRequestRejectsRawCommandAndLooseBounds(t *testing.T) {
|
||||
request := validatorSCUMTypedRCONTemplateRequest()
|
||||
request.Payload = map[string]any{"rawCommand": "#SetFamePoints 7 100"}
|
||||
request.ReviewReason = `use C:\SCUM\secret.txt`
|
||||
request.Bounds.MaxPayloadBytes = 100000
|
||||
|
||||
err := ValidateSCUMTypedRCONTemplateRequest(request)
|
||||
if err == nil || !strings.Contains(err.Error(), "payload key is unsafe") || !strings.Contains(err.Error(), "reviewReason is unsafe") || !strings.Contains(err.Error(), "maxPayloadBytes") {
|
||||
t.Fatalf("expected raw command/reason/bounds violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMTypedRCONTemplateResultAcceptsConfirmedEnvelope(t *testing.T) {
|
||||
result := validatorSCUMTypedRCONTemplateResult()
|
||||
|
||||
if err := ValidateSCUMTypedRCONTemplateResult(result); err != nil {
|
||||
t.Fatalf("expected valid typed RCON template result, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMTypedRCONTemplateResultRejectsUnconfirmedSuccessAndUnsafeSummary(t *testing.T) {
|
||||
result := validatorSCUMTypedRCONTemplateResult()
|
||||
result.ConfirmationStatus = domain.SCUMRCONConfirmationUnknown
|
||||
result.SafeSummary = "rcon password leaked"
|
||||
|
||||
err := ValidateSCUMTypedRCONTemplateResult(result)
|
||||
if err == nil || !strings.Contains(err.Error(), "confirmed") || !strings.Contains(err.Error(), "safeSummary") {
|
||||
t.Fatalf("expected confirmation and summary violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMGuardedMutationRequestAllowsBoundedGenericTemplate(t *testing.T) {
|
||||
request := validatorSCUMGuardedMutationRequest()
|
||||
|
||||
if err := ValidateSCUMGuardedMutationRequest(request); err != nil {
|
||||
t.Fatalf("expected valid guarded mutation request, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMGuardedMutationRequestRejectsRawXMLSQL855AndMissingGuards(t *testing.T) {
|
||||
request := validatorSCUMGuardedMutationRequest()
|
||||
request.Payload = map[string]any{"rawXml": "<CharacterTemplate><Attribute name=\"Strength\" value=\"9\" /></CharacterTemplate>", "fieldKey855": "855"}
|
||||
request.BackupEvidenceDigest = ""
|
||||
request.OfflineEvidenceDigest = ""
|
||||
request.DangerConfirmationDigest = ""
|
||||
request.ReadbackExpectationDigest = ""
|
||||
request.ReviewReason = "update sqlite:///private/tmp/SCUM.db directly"
|
||||
request.Bounds.MaxAffectedRows = 2
|
||||
|
||||
err := ValidateSCUMGuardedMutationRequest(request)
|
||||
if err == nil || !strings.Contains(err.Error(), "payload key is unsafe") || !strings.Contains(err.Error(), "payload value is unsafe") || !strings.Contains(err.Error(), "backupEvidenceDigest") || !strings.Contains(err.Error(), "offlineEvidenceDigest") || !strings.Contains(err.Error(), "dangerConfirmationDigest") || !strings.Contains(err.Error(), "readbackExpectationDigest") || !strings.Contains(err.Error(), "maxAffectedRows") || !strings.Contains(err.Error(), "reviewReason") {
|
||||
t.Fatalf("expected guarded mutation safety violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMGuardedMutationResultAcceptsConfirmedSingleRowEnvelope(t *testing.T) {
|
||||
result := validatorSCUMGuardedMutationResult()
|
||||
|
||||
if err := ValidateSCUMGuardedMutationResult(result); err != nil {
|
||||
t.Fatalf("expected valid guarded mutation result, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMGuardedMutationResultRejectsMultiRowMissingReadbackAndUnsafeSummary(t *testing.T) {
|
||||
result := validatorSCUMGuardedMutationResult()
|
||||
result.AffectedRows = 2
|
||||
result.ReadbackStatus = domain.SCUMMutationReadbackUnknown
|
||||
result.ReadbackDigest = ""
|
||||
result.SafeSummary = "raw <CharacterTemplate /> leaked"
|
||||
|
||||
err := ValidateSCUMGuardedMutationResult(result)
|
||||
if err == nil || !strings.Contains(err.Error(), "exactly one affected row") || !strings.Contains(err.Error(), "readback") || !strings.Contains(err.Error(), "safeSummary") {
|
||||
t.Fatalf("expected affected-row/readback/summary violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMParsedLogBatchResultAcceptsRotationSafeEnvelope(t *testing.T) {
|
||||
result := validatorSCUMParsedLogBatchResult()
|
||||
|
||||
if err := ValidateSCUMParsedLogBatchResult(result); err != nil {
|
||||
t.Fatalf("expected valid parsed log batch result, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMParsedLogBatchResultRejectsRawLineNetworkMaterialAndLooseBounds(t *testing.T) {
|
||||
result := validatorSCUMParsedLogBatchResult()
|
||||
result.ParserDigest = ""
|
||||
result.FirstCursor.SourceIdentityDigest = `C:\SCUM\Saved\Logs\login.log`
|
||||
result.Events[0].Payload = map[string]any{"rawLine": "2026.08.13: '203.0.113.10 player' logged in"}
|
||||
result.Limits.MaxLineBytes = 1000000
|
||||
|
||||
err := ValidateSCUMParsedLogBatchResult(result)
|
||||
if err == nil || !strings.Contains(err.Error(), "parserDigest") || !strings.Contains(err.Error(), "sourceIdentityDigest") || !strings.Contains(err.Error(), "payload key is unsafe") || !strings.Contains(err.Error(), "payload value is unsafe") || !strings.Contains(err.Error(), "maxLineBytes") {
|
||||
t.Fatalf("expected parsed-log safety violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMCapabilityEvidenceRequiresSafeCurrentServiceIdentity(t *testing.T) {
|
||||
evidence := domain.SCUMCapabilityEvidence{Capability: domain.SCUMDataCapabilityPlayerRead, Status: domain.SCUMCapabilityEvidenceCompatible, Binding: validatorSCUMBinding(), AdapterVersion: "adapter-1", SchemaFingerprint: scumProbeHash, ProbeResultDigest: scumProbeHash, AssetDigests: []string{scumProbeHash}, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||
if err := ValidateSCUMCapabilityEvidence(evidence); err != nil {
|
||||
t.Fatalf("expected valid evidence, got %v", err)
|
||||
}
|
||||
|
||||
evidence.SafeError = domain.SCUMSafeError{Code: domain.SCUMSafeErrorProbeFailed, Message: "sqlite:///private/tmp/SCUM.db locked"}
|
||||
if err := ValidateSCUMCapabilityEvidence(evidence); err == nil || !strings.Contains(err.Error(), "protected material") {
|
||||
t.Fatalf("expected safe error redaction violation, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func validatorSCUMBinding() domain.SCUMBindingIdentity {
|
||||
return domain.SCUMBindingIdentity{ServerInstanceID: "server-1", RunBindingID: "binding-1", RunEndpointID: "run-1", PluginID: "game.scum", PluginVersion: "0.1.6", AdapterVersion: "adapter-1", GameVersion: "scum-1", DatabaseIdentity: "db-fingerprint-1"}
|
||||
}
|
||||
|
||||
func validatorSCUMSQLiteTemplateRequest() domain.SCUMSQLiteTemplateRequest {
|
||||
return domain.SCUMSQLiteTemplateRequest{RequestID: "request-1", JobID: "job-1", Binding: validatorSCUMBinding(), Capability: domain.SCUMDataCapabilityPlayerRead, TargetKey: "scum-database", TemplateKey: "players.active.v1", AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumProbeHash, AssetDigest: scumProbeHash, ParameterDigest: scumProbeHash, Parameters: map[string]any{"cursor": "", "limit": 100.0}, Bounds: domain.DefaultSCUMSQLiteTemplateBounds(), RequestedAt: time.Now()}
|
||||
}
|
||||
|
||||
func validatorSCUMSQLiteTemplateResult() domain.SCUMSQLiteTemplateResult {
|
||||
request := validatorSCUMSQLiteTemplateRequest()
|
||||
return domain.SCUMSQLiteTemplateResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, ParameterDigest: request.ParameterDigest, SourceFingerprint: scumProbeHash, ObservedAt: time.Now(), ResultDigest: scumProbeHash, RowCount: 1, Rows: []map[string]any{{"externalPlayerId": "player-redacted", "displayName": "Known Player", "fame": 12.5, "online": true, "squadId": nil}}, Limits: request.Bounds, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||
}
|
||||
|
||||
func validatorSCUMTypedRCONTemplateRequest() domain.SCUMTypedRCONTemplateRequest {
|
||||
return domain.SCUMTypedRCONTemplateRequest{RequestID: "request-rcon-1", JobID: "job-rcon-1", Binding: validatorSCUMBinding(), Capability: domain.SCUMDataCapabilityEconomyCommand, TransportKey: "scum-rcon", TargetKey: "scum-rcon", TemplateKey: "economy.fame.set.v1", AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumProbeHash, AssetDigest: scumProbeHash, PayloadDigest: scumProbeHash, ConfirmationDigest: scumProbeHash, TargetIdentityDigest: scumProbeHash, IdempotencyKey: "idem-rcon-1", Payload: map[string]any{"externalPlayerId": "player-redacted", "absoluteValue": 100.0}, ReviewReason: "operator reviewed absolute fame update", Bounds: domain.DefaultSCUMTypedRCONTemplateBounds(), RequestedAt: time.Now()}
|
||||
}
|
||||
|
||||
func validatorSCUMTypedRCONTemplateResult() domain.SCUMTypedRCONTemplateResult {
|
||||
request := validatorSCUMTypedRCONTemplateRequest()
|
||||
return domain.SCUMTypedRCONTemplateResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TransportKey: request.TransportKey, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, PayloadDigest: request.PayloadDigest, ConfirmationDigest: request.ConfirmationDigest, TargetIdentityDigest: request.TargetIdentityDigest, ObservedAt: time.Now(), ResultDigest: scumProbeHash, ResponseDigest: scumProbeHash, ConfirmationStatus: domain.SCUMRCONConfirmationConfirmed, ConfirmationDigestID: scumProbeHash, SafeSummary: "confirmed by declared readback", Limits: request.Bounds, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||
}
|
||||
|
||||
func validatorSCUMGuardedMutationRequest() domain.SCUMGuardedMutationRequest {
|
||||
return domain.SCUMGuardedMutationRequest{RequestID: "request-mutation-1", JobID: "job-mutation-1", Binding: validatorSCUMBinding(), Capability: domain.SCUMDataCapabilityProfileXMLWrite, TargetKey: "scum-mutation-db", TemplateKey: "profile.attributes.patch.v1", AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumProbeHash, AssetDigest: scumProbeHash, TargetIdentityDigest: scumProbeHash, ExpectedRowDigest: scumProbeHash, ExpectedValueDigest: scumProbeHash, ExpectedXMLDigest: scumProbeHash, PatchDigest: scumProbeHash, BackupEvidenceDigest: scumProbeHash, OfflineEvidenceDigest: scumProbeHash, DangerConfirmationDigest: scumProbeHash, ReadbackExpectationDigest: scumProbeHash, IdempotencyKey: "idem-mutation-1", Payload: map[string]any{"attributeKey": "Strength", "absoluteValue": 8.5}, ReviewReason: "operator confirmed offline profile attribute patch", Bounds: domain.DefaultSCUMGuardedMutationBounds(), RequestedAt: time.Now()}
|
||||
}
|
||||
|
||||
func validatorSCUMGuardedMutationResult() domain.SCUMGuardedMutationResult {
|
||||
request := validatorSCUMGuardedMutationRequest()
|
||||
return domain.SCUMGuardedMutationResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, SourceFingerprint: scumProbeHash, TargetIdentityDigest: request.TargetIdentityDigest, ExpectedRowDigest: request.ExpectedRowDigest, ExpectedValueDigest: request.ExpectedValueDigest, ExpectedXMLDigest: request.ExpectedXMLDigest, PatchDigest: request.PatchDigest, BackupEvidenceDigest: request.BackupEvidenceDigest, OfflineEvidenceDigest: request.OfflineEvidenceDigest, DangerConfirmationDigest: request.DangerConfirmationDigest, ReadbackExpectationDigest: request.ReadbackExpectationDigest, ObservedAt: time.Now(), ResultDigest: scumProbeHash, BeforeDigest: scumProbeHash, AfterDigest: scumProbeHash, ReadbackDigest: scumProbeHash, AffectedRows: 1, ReadbackStatus: domain.SCUMMutationReadbackConfirmed, SafeSummary: "confirmed by declared readback", SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}, Limits: request.Bounds}
|
||||
}
|
||||
|
||||
func validatorSCUMParsedLogBatchResult() domain.SCUMParsedLogBatchResult {
|
||||
cursor := domain.SCUMParsedLogCursor{SourceIdentityDigest: scumProbeHash, StreamGeneration: scumProbeHash, Sequence: 7}
|
||||
return domain.SCUMParsedLogBatchResult{
|
||||
RequestID: "request-log-1",
|
||||
JobID: "job-log-1",
|
||||
Binding: validatorSCUMBinding(),
|
||||
Status: domain.SCUMTerminalResultSucceeded,
|
||||
SourceKey: "scum-login-events",
|
||||
StreamKey: "scum.login",
|
||||
ParserKey: "scum-login-log-login-parser",
|
||||
ParserVersion: "scum-login-log-v1",
|
||||
AdapterVersion: "adapter-1",
|
||||
AssetDigest: scumProbeHash,
|
||||
ParserDigest: scumProbeHash,
|
||||
ObservedAt: time.Now(),
|
||||
ResultDigest: scumProbeHash,
|
||||
FirstCursor: cursor,
|
||||
LastCursor: cursor,
|
||||
TailState: domain.SCUMLogTailRotated,
|
||||
Replay: true,
|
||||
EventCount: 1,
|
||||
SafeSummary: "one sanitized login event parsed from declared source",
|
||||
SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone},
|
||||
Limits: domain.DefaultSCUMParsedLogBatchBounds(),
|
||||
Events: []domain.SCUMParsedLogEvent{{
|
||||
EventType: "scum.login",
|
||||
OccurredAt: time.Now(),
|
||||
Cursor: cursor,
|
||||
LogicalEventDigest: scumProbeHash,
|
||||
EventDigest: scumProbeHash,
|
||||
PayloadDigest: scumProbeHash,
|
||||
Payload: map[string]any{"externalPlayerId": "player-redacted", "displayName": "Known Player", "profileLocalId": "profile-redacted"},
|
||||
}},
|
||||
}
|
||||
}
|
||||
@@ -244,6 +244,18 @@ describe("PlatformApiClient AI providers", () => {
|
||||
count: 1
|
||||
});
|
||||
}
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/scum/players")) return jsonResponse({ items: [{ id: "scum-player-1", gamePlayerId: "steam-1", displayName: "Prisoner One", online: true }], count: 1 });
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/scum/squads")) return jsonResponse({ items: [{ id: "squad-1", squadId: "squad-1", name: "Alpha" }], count: 1 });
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/scum/squad-members")) return jsonResponse({ items: [{ id: "member-1", squadId: "squad-1", gamePlayerId: "steam-1" }], count: 1 });
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/scum/vehicles")) return jsonResponse({ items: [{ id: "vehicle-1", vehicleId: "vehicle-1", label: "SUV" }], count: 1 });
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/scum/flags")) return jsonResponse({ items: [{ id: "flag-1", flagId: "flag-1", ownerSquadId: "squad-1" }], count: 1 });
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/scum/positions")) return jsonResponse({ items: [{ id: "position-1", subjectType: "player", subjectId: "steam-1", x: 1, y: 2, z: 3 }], count: 1 });
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/scum/operations") && (!init?.method || init.method === "GET")) return jsonResponse({ items: [], count: 0 });
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/scum/operations") && init?.method === "POST") return jsonResponse({ id: "op-1", serverInstanceId: server.id, pluginId: plugin.id, templateKey: "player.fame.set", status: "waiting", approvalLevel: "operator", createdAt: "2026-07-03T00:00:00Z", updatedAt: "2026-07-03T00:00:00Z" });
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/scum/operations/op-1/approve") && init?.method === "POST") return jsonResponse({ id: "op-1", serverInstanceId: server.id, pluginId: plugin.id, templateKey: "player.fame.set", status: "queued", approvalLevel: "operator", createdAt: "2026-07-03T00:00:00Z", updatedAt: "2026-07-03T00:00:00Z" });
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/scum/workflows") && (!init?.method || init.method === "GET")) return jsonResponse({ items: [], count: 0 });
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/scum/workflows") && init?.method === "POST") return jsonResponse({ id: "workflow-1", serverInstanceId: server.id, pluginId: plugin.id, templateKey: "scum.world-refresh", status: "queued", createdAt: "2026-07-03T00:00:00Z", updatedAt: "2026-07-03T00:00:00Z" });
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/scum/workflow-steps?workflowId=workflow-1")) return jsonResponse({ items: [{ id: "step-1", workflowId: "workflow-1", serverInstanceId: server.id, stepKey: "read-positions", status: "queued", createdAt: "2026-07-03T00:00:00Z", updatedAt: "2026-07-03T00:00:00Z" }], count: 1 });
|
||||
if (url.endsWith("/api/v1/file-operations/dispatch") && init?.method === "POST") {
|
||||
expect(JSON.parse(String(init.body))).toEqual({
|
||||
serverInstanceId: server.id,
|
||||
@@ -542,6 +554,18 @@ describe("PlatformApiClient AI providers", () => {
|
||||
await expect(client.deleteServerInstance(server.id, { password: "secret-password", force: true, confirmation: "FORCE DELETE" })).resolves.toBeUndefined();
|
||||
await expect(client.getPlatformResourceUsage()).resolves.toMatchObject({ source: "platform-derived", cpuPercent: 28 });
|
||||
await expect(client.listServerMetrics()).resolves.toMatchObject({ count: 1, items: [{ serverInstanceId: server.id, online: true }] });
|
||||
await expect(client.listSCUMPlayers(server.id)).resolves.toMatchObject({ count: 1, items: [{ gamePlayerId: "steam-1" }] });
|
||||
await expect(client.listSCUMSquads(server.id)).resolves.toMatchObject({ count: 1 });
|
||||
await expect(client.listSCUMSquadMembers(server.id)).resolves.toMatchObject({ count: 1 });
|
||||
await expect(client.listSCUMVehicles(server.id)).resolves.toMatchObject({ count: 1 });
|
||||
await expect(client.listSCUMFlags(server.id)).resolves.toMatchObject({ count: 1 });
|
||||
await expect(client.listSCUMPositions(server.id)).resolves.toMatchObject({ count: 1 });
|
||||
await expect(client.listSCUMOperations(server.id)).resolves.toMatchObject({ count: 0 });
|
||||
await expect(client.createSCUMOperation(server.id, { templateKey: "player.fame.set", playerId: "steam-1", payload: { fame: 100 }, reason: "typed correction", idempotencyKey: "idem-scum-op" })).resolves.toMatchObject({ id: "op-1", status: "waiting" });
|
||||
await expect(client.approveSCUMOperation(server.id, "op-1")).resolves.toMatchObject({ id: "op-1", status: "queued" });
|
||||
await expect(client.listSCUMWorkflows(server.id)).resolves.toMatchObject({ count: 0 });
|
||||
await expect(client.createSCUMWorkflow(server.id, { templateKey: "scum.world-refresh", idempotencyKey: "idem-scum-workflow" })).resolves.toMatchObject({ id: "workflow-1", status: "queued" });
|
||||
await expect(client.listSCUMWorkflowSteps(server.id, "workflow-1")).resolves.toMatchObject({ count: 1, items: [{ stepKey: "read-positions" }] });
|
||||
await expect(client.dispatchFileOperation({ serverInstanceId: server.id, operation: "read", key: "logs/latest.log", idempotencyKey: "idem-file" })).resolves.toMatchObject({
|
||||
status: "queued",
|
||||
job: { capability: "files.read", targetKey: "logs/latest.log" }
|
||||
@@ -600,7 +624,7 @@ describe("PlatformApiClient AI providers", () => {
|
||||
client.invokeAI({ requestId: "ai-1", serverInstanceId: server.id, purpose: "config.suggest", prompt: "Tune PVP safely", currentConfig: "server.name=Example Survival #1\n" })
|
||||
).resolves.toMatchObject({ status: "ok", usage: { mocked: true }, configRecommendation: { diffSummary: "review required" } });
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(36);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(48);
|
||||
});
|
||||
|
||||
it("calls plugin marketplace endpoints with filter and state contracts", async () => {
|
||||
@@ -629,6 +653,24 @@ describe("PlatformApiClient AI providers", () => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("surfaces SCUM typed operation failures from the platform", async () => {
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/scum/operations") && init?.method === "POST") {
|
||||
return new Response(JSON.stringify({ code: "validation", message: "SCUM operation template is not declared" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected request: ${url}`);
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const client = new PlatformApiClient();
|
||||
|
||||
await expect(client.createSCUMOperation(server.id, { templateKey: "raw.sql", reason: "unsafe", idempotencyKey: "bad-scum-op" })).rejects.toThrow("SCUM operation template is not declared");
|
||||
});
|
||||
|
||||
it("keeps raw key and base URL fields out of provider responses", () => {
|
||||
expect("apiKey" in provider).toBe(false);
|
||||
expect("rawApiKey" in provider).toBe(false);
|
||||
|
||||
@@ -101,6 +101,14 @@ import type {
|
||||
RemoteAdapterDeclarationListResponse,
|
||||
RemoteAdapterRequest,
|
||||
RemoteAdapterResponse,
|
||||
SCUMListResponse,
|
||||
SCUMOperationListResponse,
|
||||
SCUMOperationRequest,
|
||||
SCUMOperationResponse,
|
||||
SCUMWorkflowCreateRequest,
|
||||
SCUMWorkflowListResponse,
|
||||
SCUMWorkflowResponse,
|
||||
SCUMWorkflowStepListResponse,
|
||||
ServerRuntimeActionsResponse,
|
||||
UserCreateRequest,
|
||||
UserListResponse,
|
||||
@@ -575,6 +583,57 @@ export class PlatformApiClient {
|
||||
return this.request<RemoteAdapterResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/remote-adapters`, { method: "POST", body: request });
|
||||
}
|
||||
|
||||
async listSCUMPlayers(serverInstanceId: string): Promise<SCUMListResponse> {
|
||||
return this.request<SCUMListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/players`);
|
||||
}
|
||||
|
||||
async listSCUMSquads(serverInstanceId: string): Promise<SCUMListResponse> {
|
||||
return this.request<SCUMListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/squads`);
|
||||
}
|
||||
|
||||
async listSCUMSquadMembers(serverInstanceId: string): Promise<SCUMListResponse> {
|
||||
return this.request<SCUMListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/squad-members`);
|
||||
}
|
||||
|
||||
async listSCUMVehicles(serverInstanceId: string): Promise<SCUMListResponse> {
|
||||
return this.request<SCUMListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/vehicles`);
|
||||
}
|
||||
|
||||
async listSCUMFlags(serverInstanceId: string): Promise<SCUMListResponse> {
|
||||
return this.request<SCUMListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/flags`);
|
||||
}
|
||||
|
||||
async listSCUMPositions(serverInstanceId: string): Promise<SCUMListResponse> {
|
||||
return this.request<SCUMListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/positions`);
|
||||
}
|
||||
|
||||
async listSCUMOperations(serverInstanceId: string): Promise<SCUMOperationListResponse> {
|
||||
return this.request<SCUMOperationListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/operations`);
|
||||
}
|
||||
|
||||
async createSCUMOperation(serverInstanceId: string, request: SCUMOperationRequest): Promise<SCUMOperationResponse> {
|
||||
return this.request<SCUMOperationResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/operations`, { method: "POST", body: request });
|
||||
}
|
||||
|
||||
async approveSCUMOperation(serverInstanceId: string, operationId: string): Promise<SCUMOperationResponse> {
|
||||
return this.request<SCUMOperationResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/operations/${encodeURIComponent(operationId)}/approve`, { method: "POST", body: {} });
|
||||
}
|
||||
|
||||
async listSCUMWorkflows(serverInstanceId: string): Promise<SCUMWorkflowListResponse> {
|
||||
return this.request<SCUMWorkflowListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/workflows`);
|
||||
}
|
||||
|
||||
async createSCUMWorkflow(serverInstanceId: string, request: SCUMWorkflowCreateRequest): Promise<SCUMWorkflowResponse> {
|
||||
return this.request<SCUMWorkflowResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/workflows`, { method: "POST", body: request });
|
||||
}
|
||||
|
||||
async listSCUMWorkflowSteps(serverInstanceId: string, workflowId?: string): Promise<SCUMWorkflowStepListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (workflowId) params.set("workflowId", workflowId);
|
||||
const query = params.toString();
|
||||
return this.request<SCUMWorkflowStepListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/workflow-steps${query ? `?${query}` : ""}`);
|
||||
}
|
||||
|
||||
async dispatchFileOperation(request: FileOperationDispatchRequest): Promise<FileOperationDispatchResponse> {
|
||||
return this.request<FileOperationDispatchResponse>("/file-operations/dispatch", {
|
||||
method: "POST",
|
||||
|
||||
@@ -6,7 +6,7 @@ API clients and DTO types live here, not inside page components.
|
||||
|
||||
- `users`: user and role APIs.
|
||||
- `serverPlugins`: plugin marketplace and installed plugin APIs.
|
||||
- `serverInstances`: create server, lifecycle, deployment/member/detail APIs, and SCUM local resource read APIs.
|
||||
- `serverInstances`: create server, lifecycle, deployment/member/detail APIs, and SCUM typed projection/workflow APIs.
|
||||
- `aiProviders`: provider CRUD, test, and model APIs.
|
||||
- `jobs`: job status and operation APIs.
|
||||
- `runEndpoints`: run endpoint status, lifecycle capabilities, and capacity APIs.
|
||||
@@ -26,7 +26,7 @@ Normal browser login uses the platform's HttpOnly SameSite cookie and `credentia
|
||||
- `getServerRuntimeBinding` reads `/server-instances/{id}/runtime-binding`; `updateServerRuntimeBinding` patches the selected profile and logical refs for internal/advanced logical transports. Server detail must not expose a manual runtime-binding tab or require these fields before normal start/stop when plugin-declared deployment/lifecycle data is sufficient. Responses contain only profile metadata, logical key names, configured/secret-backed flags, missing keys, and safe reasons. They never contain stored refs or secret values.
|
||||
- `startServerInstance` and `stopServerInstance` post `ServerLifecycleCommandRequest` with the current config version and receive the lifecycle job response.
|
||||
- `listServerAdministratorCandidates`, `addServerAdministrator`, and `removeServerAdministrator` call server membership endpoints so server owners can invite or remove active non-platform-admin server administrators.
|
||||
- SCUM local resource reads use `listSCUMPlayers`, `listSCUMSquads`, `listSCUMSquadMembers`, `listSCUMVehicles`, `listSCUMFlags`, and `listSCUMPositions`. Removed legacy SCUM execution endpoints have no frontend client wrappers; future writes must use the reviewed named-field/gift contracts and must never expose SQL text, RCON text, DSNs, host paths, or protected payloads.
|
||||
- SCUM projection reads use `listSCUMPlayers`, `listSCUMSquads`, `listSCUMSquadMembers`, `listSCUMVehicles`, `listSCUMFlags`, and `listSCUMPositions`; SCUM writes use `createSCUMOperation`, `approveSCUMOperation`, `createSCUMWorkflow`, and workflow/step list APIs. These APIs expose only projection rows, typed template keys, status, and safe summaries, never SQL text, RCON text, DSNs, host paths, or protected payloads.
|
||||
- `dispatchFileOperation` posts `FileOperationDispatchRequest` to `/file-operations/dispatch` using logical file keys and scoped refs rather than raw host paths; it is not wired into SCUM server-detail/plugin pages as a raw file workbench.
|
||||
- `listArtifacts`, `openArtifactDownload`, and `readArtifactContent` use platform artifact routes for available job/server artifacts. Browser reads are chunked through `/artifacts/{id}/content` and must render only safe filenames, checksums, progress, and platform storage behavior.
|
||||
- `authorizePluginBridge` posts `PluginBridgeAuthorizeRequest` to `/plugin-bridge/authorize` for preflight decisions.
|
||||
|
||||
@@ -1373,6 +1373,17 @@ export interface RemoteAdapterResponse {
|
||||
completedAt?: string;
|
||||
}
|
||||
|
||||
export type SCUMJsonRecord = Record<string, unknown>;
|
||||
export interface SCUMListResponse<T = SCUMJsonRecord> { items: T[]; count: number; }
|
||||
export interface SCUMWorkflowCreateRequest { templateKey: string; idempotencyKey: string; input?: SCUMJsonRecord; }
|
||||
export interface SCUMOperationRequest { templateKey: string; playerId?: string; payload?: SCUMJsonRecord; guard?: SCUMJsonRecord; reason: string; idempotencyKey: string; }
|
||||
export interface SCUMWorkflowResponse { id: string; serverInstanceId: string; pluginId: string; templateKey: string; requestedBy?: string; idempotencyKey?: string; status: string; currentStepKey?: string; input?: SCUMJsonRecord; safeSummary?: SCUMJsonRecord; blockerReason?: string; auditReferences?: string[]; createdAt: string; updatedAt: string; completedAt?: string; }
|
||||
export interface SCUMWorkflowStepResponse { id: string; workflowId: string; serverInstanceId: string; stepKey: string; dependsOn?: string[]; status: string; operationKey?: string; queryTemplateKey?: string; capability?: string; targetKey?: string; jobId?: string; attempt?: number; maxAttempts?: number; mutatesState?: boolean; confirmation?: SCUMJsonRecord; safeSummary?: SCUMJsonRecord; blockerReason?: string; auditReferences?: string[]; createdAt: string; updatedAt: string; completedAt?: string; }
|
||||
export interface SCUMOperationResponse { id: string; serverInstanceId: string; pluginId: string; templateKey: string; playerId?: string; requesterId?: string; approverId?: string; approvalLevel: string; payload?: SCUMJsonRecord; guard?: SCUMJsonRecord; confirmation?: SCUMJsonRecord; status: string; reason?: string; runJobId?: string; safeSummary?: SCUMJsonRecord; auditReferences?: string[]; createdAt: string; approvedAt?: string; completedAt?: string; updatedAt: string; }
|
||||
export type SCUMWorkflowListResponse = SCUMListResponse<SCUMWorkflowResponse>;
|
||||
export type SCUMWorkflowStepListResponse = SCUMListResponse<SCUMWorkflowStepResponse>;
|
||||
export type SCUMOperationListResponse = SCUMListResponse<SCUMOperationResponse>;
|
||||
|
||||
export interface ServerConfigResponse {
|
||||
serverInstanceId: string;
|
||||
configVersion: number;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Shared Visual Contract
|
||||
|
||||
All first-party pages inherit the platform_web game-operations style with black-mecha default materials and a selectable magical-girl theme. Page implementations must use shared theme tokens and surface classes so 首页、服务器管理、插件市场、用户管理、AI 提供商管理、系统维护, server details, drawers, dialogs, safe diffs, plugin-declared pages, and job/status surfaces all feel like one console.
|
||||
All first-party pages inherit the platform_web game-operations style with black-mecha default materials and a selectable magical-girl theme. Page implementations must use shared theme tokens and surface classes so 首页、服务器管理、插件市场、用户管理、AI 提供商管理、系统维护, server details, drawers, dialogs, safe diffs, plugin-declared pages, and workflow/status surfaces all feel like one console.
|
||||
|
||||
- Major surfaces remain transparent jelly/glass panels with visible background desktop, icy rim light, diamond borders, shine sweeps, and candy-color accents.
|
||||
- Built-in magical desktops and user-uploaded backgrounds render behind readable contrast surfaces.
|
||||
@@ -23,7 +23,7 @@ Default landing page for server owners and server administrators. Shows searchab
|
||||
|
||||
## 服务器详情
|
||||
|
||||
Daily operations hub for one server. Status header shows online state, player count, TPS, latency, CPU/memory/disk progress, metric freshness, and confirmed start/stop lifecycle actions. Plugin-declared pages render as first-class server tabs before platform sections, so each game owns its safe menu surface; SCUM detail uses exactly 用户管理, 队伍管理, 实时地图, 礼包管理, and AI 助手. SCUM deployment and administrator settings move to compact header/list actions instead of a permanent management tab, and AI suggestions produce reviewable config diffs or named-field drafts without raw AI keys reaching the frontend. Raw logs, management terminal/RCON input, arbitrary config workbench, generic operation history, runtime-binding, and generic plugin-control tabs must not be exposed in server detail.
|
||||
Daily operations hub for one server. Status header shows online state, player count, TPS, latency, CPU/memory/disk progress, metric freshness, and confirmed start/stop lifecycle actions. Plugin-declared pages render as first-class server tabs before platform sections, so each game owns its safe menu surface; SCUM pages use projection-backed users, squads, map, gifts, and workflows. Built-in sections are 管理 (deployment status, metadata, administrators) and AI 助手 (LLM suggestions produce reviewable config diffs or typed workflow drafts; no raw AI keys reach the frontend). Raw logs, management terminal/RCON input, arbitrary config workbench, generic operation history, runtime-binding, and generic plugin-control tabs must not be exposed in server detail.
|
||||
|
||||
## 插件市场
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ Plugin page runs with safe platform context.
|
||||
- `plugin-lifecycle.request`: declared plugin lifecycle request through Platform.
|
||||
- `ai.invoke`: platform-mediated AI invocation.
|
||||
|
||||
The host intersects manifest-level and page-level permissions/actions before exposing context. SCUM pages receive only declared local-resource actions for 用户管理, 队伍管理, 实时地图, 礼包管理, and AI 助手; the host does not synthesize undeclared SCUM semantics.
|
||||
The host intersects manifest-level and page-level permissions/actions before exposing context. The SCUM operations page additionally intersects its command, snapshot, and query-template keys with `gameClientBridge.pages.operations`; it does not synthesize undeclared SCUM semantics.
|
||||
|
||||
## Forbidden
|
||||
|
||||
|
||||
@@ -1 +1,14 @@
|
||||
export interface PluginPageWorkspaceActions {}
|
||||
export interface PluginPageWorkspaceActions {
|
||||
listSCUMPlayers?: () => Promise<unknown>;
|
||||
listSCUMSquads?: () => Promise<unknown>;
|
||||
listSCUMSquadMembers?: () => Promise<unknown>;
|
||||
listSCUMVehicles?: () => Promise<unknown>;
|
||||
listSCUMFlags?: () => Promise<unknown>;
|
||||
listSCUMPositions?: () => Promise<unknown>;
|
||||
listSCUMOperations?: () => Promise<unknown>;
|
||||
createSCUMOperation?: (request: unknown) => Promise<unknown>;
|
||||
approveSCUMOperation?: (operationId: string) => Promise<unknown>;
|
||||
listSCUMWorkflows?: () => Promise<unknown>;
|
||||
createSCUMWorkflow?: (request: unknown) => Promise<unknown>;
|
||||
listSCUMWorkflowSteps?: (workflowId?: string) => Promise<unknown>;
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ describe("first-party console pages", () => {
|
||||
expect(html).not.toContain("操作历史");
|
||||
});
|
||||
|
||||
it("routes SCUM management through plugin pages and the AI assistant", () => {
|
||||
it("routes SCUM operations through typed plugin workflow surfaces", () => {
|
||||
expect(serverDetailPageSource).toContain("PluginPageHostPage");
|
||||
expect(serverDetailPageSource).toContain("plugin:${page.key}");
|
||||
expect(serverDetailPageSource).toContain("section === \"llm\"");
|
||||
|
||||
@@ -96,18 +96,18 @@ describe("PluginPageHostPage", () => {
|
||||
expect(hostSource).not.toMatch(/ScumFileConfigWorkbench|GamePlayerIntelligencePanel|GameGiftCatalogPanel|ScumMapTrajectoryPanel|game\.scum/);
|
||||
});
|
||||
|
||||
it("removes SCUM legacy workspace callbacks from the parent host", () => {
|
||||
it("keeps typed SCUM workspace callbacks stable across parent operational refreshes", () => {
|
||||
expect(hostSource).toContain("readyPluginRef.current = readyPlugin");
|
||||
expect(hostSource).toContain("hostContextRef.current = hostContext");
|
||||
expect(hostSource).not.toContain("listSCUMPlayers");
|
||||
expect(hostSource).not.toContain("listSCUMPositions");
|
||||
expect(hostSource).not.toContain("createSCUMWorkflow");
|
||||
expect(hostSource).not.toContain("createSCUMOperation");
|
||||
expect(hostSource).not.toContain("listSCUMWorkflowSteps");
|
||||
expect(hostSource).toContain("listSCUMPlayers: () => platformApiClient.listSCUMPlayers(serverId)");
|
||||
expect(hostSource).toContain("createSCUMWorkflow: (request) => platformApiClient.createSCUMWorkflow(serverId, request as never)");
|
||||
expect(hostSource).toContain("createSCUMOperation: (request) => platformApiClient.createSCUMOperation(serverId, request as never)");
|
||||
expect(hostSource).toContain("listSCUMWorkflowSteps: (workflowId) => platformApiClient.listSCUMWorkflowSteps(serverId, workflowId)");
|
||||
expect(hostSource).not.toContain("refreshWorkspace");
|
||||
expect(hostSource).not.toContain("requestFile");
|
||||
expect(hostSource).not.toContain("writeFile");
|
||||
expect(hostSource).not.toContain("getDeclaredFileReadSnapshot");
|
||||
expect(hostSource).toContain("const workspaceActions = undefined");
|
||||
expect(hostSource).toContain("}, [pluginId, serverId]);");
|
||||
expect(hostSource).not.toContain("}, [hostContext, readyPlugin, serverId]);");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin, embedded
|
||||
const [state, setState] = useState<PluginPageState>(() => initialPlugin ? { status: "ready", plugin: initialPlugin } : { status: "loading" });
|
||||
const [bundle, setBundle] = useState<ComponentType<{ context: ReturnType<typeof createPluginBridgeHostContext>; workspaceActions?: PluginPageWorkspaceActions; availability: PluginPageAvailability }> | null>(null);
|
||||
const [bundleError, setBundleError] = useState("");
|
||||
const [availability, setAvailability] = useState<PluginPageAvailability>({ available: false, reason: "正在验证本地数据通道。" });
|
||||
const [availability, setAvailability] = useState<PluginPageAvailability>({ available: false, reason: "正在验证 Companion 可用性。" });
|
||||
const readyPluginRef = useRef<GamePluginResponse | undefined>(undefined);
|
||||
const hostContextRef = useRef<ReturnType<typeof createPluginBridgeHostContext> | undefined>(undefined);
|
||||
|
||||
@@ -66,7 +66,23 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin, embedded
|
||||
}) : undefined, [manifestContract, routeKey, serverId]);
|
||||
readyPluginRef.current = readyPlugin;
|
||||
hostContextRef.current = hostContext;
|
||||
const workspaceActions = undefined as PluginPageWorkspaceActions | undefined;
|
||||
const workspaceActions = useMemo<PluginPageWorkspaceActions | undefined>(() => {
|
||||
if (!pluginId) return undefined;
|
||||
return {
|
||||
listSCUMPlayers: () => platformApiClient.listSCUMPlayers(serverId),
|
||||
listSCUMSquads: () => platformApiClient.listSCUMSquads(serverId),
|
||||
listSCUMSquadMembers: () => platformApiClient.listSCUMSquadMembers(serverId),
|
||||
listSCUMVehicles: () => platformApiClient.listSCUMVehicles(serverId),
|
||||
listSCUMFlags: () => platformApiClient.listSCUMFlags(serverId),
|
||||
listSCUMPositions: () => platformApiClient.listSCUMPositions(serverId),
|
||||
listSCUMOperations: () => platformApiClient.listSCUMOperations(serverId),
|
||||
createSCUMOperation: (request) => platformApiClient.createSCUMOperation(serverId, request as never),
|
||||
approveSCUMOperation: (operationId) => platformApiClient.approveSCUMOperation(serverId, operationId),
|
||||
listSCUMWorkflows: () => platformApiClient.listSCUMWorkflows(serverId),
|
||||
createSCUMWorkflow: (request) => platformApiClient.createSCUMWorkflow(serverId, request as never),
|
||||
listSCUMWorkflowSteps: (workflowId) => platformApiClient.listSCUMWorkflowSteps(serverId, workflowId)
|
||||
};
|
||||
}, [pluginId, serverId]);
|
||||
const bundleLoadKey = declaredBundlePage ? [declaredBundlePage.bundleKey, declaredBundlePage.bundleVersion, declaredBundlePage.bundleIntegritySha256, declaredBundlePage.path].join(":") : "";
|
||||
const loadableBundlePage = useMemo(() => declaredBundlePage, [bundleLoadKey]);
|
||||
useEffect(() => {
|
||||
@@ -78,7 +94,7 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin, embedded
|
||||
if (!serverId) { setAvailability({ available: false, reason: "插件页面没有绑定服务器。" }); return () => { active = false; }; }
|
||||
void platformApiClient.getGameClientBridgeStatus(serverId).then((status) => {
|
||||
if (active) setAvailability({ available: status.available, reason: status.reason, features: status.features });
|
||||
}).catch((error) => { if (active) setAvailability({ available: false, reason: error instanceof Error ? error.message : "无法验证本地数据通道。" }); });
|
||||
}).catch((error) => { if (active) setAvailability({ available: false, reason: error instanceof Error ? error.message : "无法验证 Companion 可用性。" }); });
|
||||
return () => { active = false; };
|
||||
}, [bundleLoadKey, loadableBundlePage, serverId]);
|
||||
|
||||
@@ -127,7 +143,7 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin, embedded
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="action-list" aria-label="plugin page declarations"><span><strong>Bundle</strong> {page.bundleKey ? `${page.bundleKey}@${page.bundleVersion}` : "未声明"}</span><span><strong>完整性</strong> {page.bundleIntegritySha256 ? `${page.bundleIntegritySha256.slice(0, 18)}…` : "未声明"}</span><span><strong>数据通道</strong> {availability.available ? "可用" : "不可用"}</span></div>
|
||||
<div className="action-list" aria-label="plugin page declarations"><span><strong>Bundle</strong> {page.bundleKey ? `${page.bundleKey}@${page.bundleVersion}` : "未声明"}</span><span><strong>完整性</strong> {page.bundleIntegritySha256 ? `${page.bundleIntegritySha256.slice(0, 18)}…` : "未声明"}</span><span><strong>Companion</strong> {availability.available ? "可用" : "不可用"}</span></div>
|
||||
</section>
|
||||
{bundleError && <ErrorState title="插件页面不可用" reason={bundleError} />}
|
||||
{!bundle && !bundleError && <LoadingState label="正在校验并加载插件页面 bundle…" />}
|
||||
|
||||
@@ -135,11 +135,6 @@ describe("ServerDetailPage config write approval", () => {
|
||||
expect(serverDetailPageSource).not.toContain("ScumFileManagementSection");
|
||||
});
|
||||
|
||||
it("keeps SCUM detail tabs to plugin management pages plus AI assistant", () => {
|
||||
expect(serverDetailPageSource).toContain('plugin?.id === "game.scum"');
|
||||
expect(serverDetailPageSource).toContain('return [...pluginPages, { id: "llm", label: "AI 助手" }]');
|
||||
});
|
||||
|
||||
it("keeps plugin lifecycle output out of raw bridge-visible detail surfaces", () => {
|
||||
expect(serverDetailPageSource).not.toContain("platformApiClient.openArtifactDownload(artifact.id)");
|
||||
expect(serverDetailPageSource).not.toContain("downloadArtifactReference(reference");
|
||||
|
||||
@@ -310,7 +310,6 @@ export function ServerDetailPage(props: PageComponentProps) {
|
||||
|
||||
function serverDetailSectionEntries(plugin?: GamePluginResponse): Array<{ id: ServerDetailSection; label: string }> {
|
||||
const pluginPages = (plugin?.pages ?? []).map((page) => ({ id: `plugin:${page.key}` as ServerDetailSection, label: page.title }));
|
||||
if (plugin?.id === "game.scum") return [...pluginPages, { id: "llm", label: "AI 助手" }];
|
||||
return [...pluginPages, ...serverDetailSections];
|
||||
}
|
||||
|
||||
|
||||
@@ -44,11 +44,11 @@ describe("console shell routes", () => {
|
||||
});
|
||||
|
||||
it("round-trips hosted plugin page hashes with server context", () => {
|
||||
const hash = hashForPage("pluginPage", { pluginId: "game.scum", routeKey: "players", serverId: "server/scum-1" });
|
||||
expect(hash).toBe("#/plugin-pages/game.scum/players?serverInstanceId=server%2Fscum-1");
|
||||
const hash = hashForPage("pluginPage", { pluginId: "game.scum", routeKey: "operations", serverId: "server/scum-1" });
|
||||
expect(hash).toBe("#/plugin-pages/game.scum/operations?serverInstanceId=server%2Fscum-1");
|
||||
const resolved = resolveRouteHash(hash, platformAdmin);
|
||||
expect(resolved.route).toMatchObject({ id: "pluginPage", showInNav: false, requiredCapability: "servers.read" });
|
||||
expect(resolved.params).toEqual({ pluginId: "game.scum", routeKey: "players", serverId: "server/scum-1" });
|
||||
expect(resolved.params).toEqual({ pluginId: "game.scum", routeKey: "operations", serverId: "server/scum-1" });
|
||||
});
|
||||
|
||||
it("keeps route metadata available for shell navigation", () => {
|
||||
|
||||
@@ -756,7 +756,7 @@ to{transform:translate(-50%,-50%) rotate(calc(var(--construct-drift) + 360deg))}
|
||||
.console-stat-strip>div,.operations-pulse-strip>div{display:grid;gap:3px;min-width:0;padding:9px 10px;border:1px solid color-mix(in srgb,var(--line) 78%,transparent);border-radius:6px;background:color-mix(in srgb,var(--surface-solid) 78%,var(--accent-soft))}
|
||||
.console-stat-strip dt,.operations-pulse-strip dt{color:var(--ink-faint);font-size:11px}
|
||||
.console-stat-strip dd,.operations-pulse-strip dd{margin:0;color:var(--ink);font-size:18px;font-weight:850}
|
||||
.map-local-board{position:relative;min-height:320px;border:1px solid color-mix(in srgb,var(--line) 76%,transparent);border-radius:14px;overflow:hidden;background:radial-gradient(circle at 50% 50%,color-mix(in srgb,var(--accent-soft) 42%,transparent),transparent 58%),linear-gradient(135deg,color-mix(in srgb,var(--surface-solid) 78%,#000),#05070d)}.map-local-dot{position:absolute;width:9px;height:9px;border-radius:999px;background:var(--accent);box-shadow:0 0 16px color-mix(in srgb,var(--accent) 80%,transparent);transform:translate(-50%,-50%)}
|
||||
.map-projection-board{position:relative;min-height:320px;border:1px solid color-mix(in srgb,var(--line) 76%,transparent);border-radius:14px;overflow:hidden;background:radial-gradient(circle at 50% 50%,color-mix(in srgb,var(--accent-soft) 42%,transparent),transparent 58%),linear-gradient(135deg,color-mix(in srgb,var(--surface-solid) 78%,#000),#05070d)}.map-projection-dot{position:absolute;width:9px;height:9px;border-radius:999px;background:var(--accent);box-shadow:0 0 16px color-mix(in srgb,var(--accent) 80%,transparent);transform:translate(-50%,-50%)}
|
||||
.console-row-list,.operations-endpoint-list,.operations-job-list{display:grid;gap:6px;margin-top:10px}
|
||||
.console-row,.operations-endpoint-row,.operations-job-row{display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:10px;min-width:0;padding:8px 10px;border:1px solid var(--line);border-radius:6px;background:var(--control-surface);color:var(--ink-soft);text-align:left}
|
||||
.console-row-button,.operations-job-row{width:100%;cursor:pointer}
|
||||
|
||||
@@ -23,7 +23,7 @@ describe("safeDiagnosticText", () => {
|
||||
});
|
||||
|
||||
it("preserves safe operational wording instead of matching labels alone", () => {
|
||||
const safe = "密钥状态已配置;Base URL 由平台托管;token 不会下发;RCON 数据不会下发。";
|
||||
const safe = "密钥状态已配置;Base URL 由平台托管;token 不会下发;RCON 数据不会投影。";
|
||||
expect(safeDiagnosticText(safe)).toBe(safe);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"key": "scum-login-log-parser-v1",
|
||||
"parserVersion": "scum-login-log-v1",
|
||||
"lineEncoding": "utf-16le",
|
||||
"maxLineBytes": 4096,
|
||||
"acceptedTemplate": "{timestamp}: '{network} {external_player_id}:{display_name}({profile_local_id})' logged {in|out} at: X={coordinate} Y={coordinate} Z={coordinate}",
|
||||
"eventTypes": ["scum.login", "scum.logout"],
|
||||
"transportCursor": ["sourceIdentity", "streamGeneration", "sequence"],
|
||||
"logicalEventIdentity": {
|
||||
"algorithm": "sha256",
|
||||
"fields": ["parserVersion", "serverId", "eventType", "occurredAtSourceText", "externalPlayerId", "displayName", "profileLocalId"],
|
||||
"excludedFields": ["network", "x", "y", "z", "sourceIdentity", "streamGeneration", "sequence"]
|
||||
},
|
||||
"privacy": {
|
||||
"stripNetworkIdentifiers": true,
|
||||
"stripCoordinatesFromIdentity": true,
|
||||
"rawLineStorage": "forbidden"
|
||||
},
|
||||
"diagnostics": ["invalid-transport-cursor", "out-of-order-transport", "duplicate-transport-cursor", "partial-line", "oversized-line", "undecodable-line", "failed-login", "malformed-line", "duplicate-logical-event"]
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"key": "scum-current-service-coordinate-map",
|
||||
"mapVersion": "scum-current-service-coordinate-map-v1",
|
||||
"adapterVersion": "scum-live-data-v1",
|
||||
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
|
||||
"sourceEvidence": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
|
||||
"authorization": {
|
||||
"redistribution": "first-party-generated-coordinate-metadata",
|
||||
"baseMapArtwork": "not-packaged",
|
||||
"renderingAvailability": "unavailable-until-authorized-base-map"
|
||||
},
|
||||
"worldBounds": {
|
||||
"minX": -901009.4375,
|
||||
"minY": -883992.5625,
|
||||
"maxX": 612580.0625,
|
||||
"maxY": 615977.375
|
||||
},
|
||||
"image": {
|
||||
"width": 4096,
|
||||
"height": 4096
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"key": "players",
|
||||
"capability": "positions.read",
|
||||
"subjectType": "player",
|
||||
"label": "Players",
|
||||
"sourceQueryKey": "scum-positions-read"
|
||||
},
|
||||
{
|
||||
"key": "vehicles",
|
||||
"capability": "positions.read",
|
||||
"subjectType": "vehicle",
|
||||
"label": "Vehicles",
|
||||
"sourceQueryKey": "scum-positions-read"
|
||||
},
|
||||
{
|
||||
"key": "flags",
|
||||
"capability": "positions.read",
|
||||
"subjectType": "flag",
|
||||
"label": "Flags",
|
||||
"sourceQueryKey": "scum-positions-read"
|
||||
}
|
||||
],
|
||||
"transformAssetPath": "assets/scum-live/map/current-service-transform.json"
|
||||
}
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
{
|
||||
"key": "scum-current-service-coordinate-transform",
|
||||
"transformVersion": "scum-current-service-transform-v1",
|
||||
"mapAssetKey": "scum-current-service-coordinate-map",
|
||||
"adapterVersion": "scum-live-data-v1",
|
||||
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
|
||||
"coordinateSystem": "current-service-observed-entity-envelope",
|
||||
"sourceEvidence": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
|
||||
"worldBounds": {
|
||||
"minX": -901009.4375,
|
||||
"minY": -883992.5625,
|
||||
"maxX": 612580.0625,
|
||||
"maxY": 615977.375
|
||||
},
|
||||
"image": {
|
||||
"width": 4096,
|
||||
"height": 4096
|
||||
},
|
||||
"axisMapping": {
|
||||
"x": "left-to-right",
|
||||
"y": "bottom-to-top-inverted-for-image-y"
|
||||
},
|
||||
"formula": {
|
||||
"pixelX": "((x - minX) / (maxX - minX)) * (width - 1)",
|
||||
"pixelY": "((maxY - y) / (maxY - minY)) * (height - 1)"
|
||||
},
|
||||
"validation": {
|
||||
"rejectNonFinite": true,
|
||||
"rejectOutOfBounds": true,
|
||||
"acceptBoundaryPoints": true
|
||||
},
|
||||
"fixtures": [
|
||||
{
|
||||
"name": "observed-minimum-corner",
|
||||
"world": { "x": -901009.4375, "y": -883992.5625 },
|
||||
"pixel": { "x": 0, "y": 4095 }
|
||||
},
|
||||
{
|
||||
"name": "observed-maximum-corner",
|
||||
"world": { "x": 612580.0625, "y": 615977.375 },
|
||||
"pixel": { "x": 4095, "y": 0 }
|
||||
},
|
||||
{
|
||||
"name": "observed-center",
|
||||
"world": { "x": -144214.6875, "y": -134007.59375 },
|
||||
"pixel": { "x": 2047.5, "y": 2047.5 }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"key": "scum-flags-read-v1",
|
||||
"adapterVersion": "scum-live-data-v1",
|
||||
"queryVersion": "scum-sqlite-query-v1",
|
||||
"capability": "flags.read",
|
||||
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
|
||||
"statementType": "single-select-or-cte",
|
||||
"statement": "SELECT bef.element_id AS flagElementId, be.element_id AS baseElementId, b.id AS baseId, b.owner_profile_id AS ownerProfileId, up.user_id AS ownerExternalPlayerId, up.name AS ownerDisplayName, CAST(NULL AS INTEGER) AS ownerSquadId, CAST(NULL AS TEXT) AS ownerSquadName, be.location_x AS flagX, be.location_y AS flagY, be.location_z AS flagZ, b.location_x AS territoryX, b.location_y AS territoryY, CAST(NULL AS REAL) AS territoryZ FROM base_element_flag bef INNER JOIN base_element be ON be.element_id = bef.element_id INNER JOIN base b ON b.id = be.element_id LEFT JOIN user_profile up ON up.id = b.owner_profile_id ORDER BY bef.element_id ASC LIMIT :limit OFFSET :offset",
|
||||
"parameters": [
|
||||
{ "name": "limit", "binding": ":limit", "type": "integer", "required": true, "minimum": 1, "maximum": 200 },
|
||||
{ "name": "offset", "binding": ":offset", "type": "integer", "required": true, "minimum": 0, "maximum": 1000000 }
|
||||
],
|
||||
"safety": {
|
||||
"queryOnly": true,
|
||||
"readOnlyConnection": true,
|
||||
"forbidMultipleStatements": true,
|
||||
"forbidAttach": true,
|
||||
"forbidWritePragmas": true,
|
||||
"forbidExtensionLoading": true,
|
||||
"maxRows": 200,
|
||||
"timeoutMs": 2000,
|
||||
"maxResultBytes": 65536
|
||||
},
|
||||
"evidence": {
|
||||
"source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
|
||||
"provenJoins": ["base_element_flag.element_id -> base_element.element_id", "base_element.element_id -> base.id", "base.owner_profile_id -> user_profile.id"],
|
||||
"ambiguousFieldsKeptNull": ["ownerSquadId", "ownerSquadName", "territoryZ"]
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"key": "scum-player-details-read-v1",
|
||||
"adapterVersion": "scum-live-data-v1",
|
||||
"queryVersion": "scum-sqlite-query-v1",
|
||||
"capability": "player-details.read",
|
||||
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
|
||||
"statementType": "single-select-or-cte",
|
||||
"statement": "SELECT u.id AS externalPlayerId, u.id_type AS idType, u.provider AS provider, u.creation_time AS userCreationTime, u.last_login_time AS userLastLoginTime, u.is_banned AS isBanned, up.id AS profileId, up.name AS displayName, up.type AS profileType, p.id AS prisonerId, p.user_profile_id AS prisonerUserProfileId, p.last_save_time AS prisonerLastSaveTime, pe.entity_id AS entityId, CASE WHEN up.template_xml IS NULL THEN NULL ELSE length(up.template_xml) END AS profileXmlLength FROM \"user\" u LEFT JOIN user_profile up ON up.user_id = u.id AND up.type = 1 LEFT JOIN prisoner p ON p.id = up.prisoner_id AND p.user_profile_id = up.id LEFT JOIN prisoner_entity pe ON pe.prisoner_id = p.id WHERE u.id = :externalPlayerId LIMIT 1",
|
||||
"parameters": [
|
||||
{ "name": "externalPlayerId", "binding": ":externalPlayerId", "type": "string", "required": true, "minLength": 1, "maxLength": 96 }
|
||||
],
|
||||
"safety": {
|
||||
"queryOnly": true,
|
||||
"readOnlyConnection": true,
|
||||
"forbidMultipleStatements": true,
|
||||
"forbidAttach": true,
|
||||
"forbidWritePragmas": true,
|
||||
"forbidExtensionLoading": true,
|
||||
"maxRows": 1,
|
||||
"timeoutMs": 2000,
|
||||
"maxResultBytes": 32768
|
||||
},
|
||||
"evidence": {
|
||||
"source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
|
||||
"provenFields": ["user.id", "user.id_type", "user.provider", "user.creation_time", "user.last_login_time", "user.is_banned", "user_profile.id", "user_profile.name", "user_profile.type", "user_profile.template_xml", "prisoner.id", "prisoner.user_profile_id", "prisoner.last_save_time", "prisoner_entity.entity_id"]
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"key": "scum-player-economy-read-v1",
|
||||
"adapterVersion": "scum-live-data-v1",
|
||||
"queryVersion": "scum-sqlite-query-v1",
|
||||
"capability": "player-details.read",
|
||||
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
|
||||
"statementType": "single-select-or-cte",
|
||||
"statement": "SELECT u.id AS externalPlayerId, up.id AS profileId, bar.id AS bankAccountId, bac.id AS currencyRowId, bac.currency_type AS currencyType, bac.account_balance AS accountBalance FROM \"user\" u JOIN user_profile up ON up.user_id = u.id AND up.type = 1 JOIN bank_account_registry bar ON bar.account_owner_user_profile_id = up.id JOIN bank_account_registry_currencies bac ON bac.bank_account_id = bar.id WHERE u.id = :externalPlayerId ORDER BY bac.currency_type ASC, bac.id ASC LIMIT :limit",
|
||||
"parameters": [
|
||||
{ "name": "externalPlayerId", "binding": ":externalPlayerId", "type": "string", "required": true, "minLength": 1, "maxLength": 96 },
|
||||
{ "name": "limit", "binding": ":limit", "type": "integer", "required": true, "minimum": 1, "maximum": 16 }
|
||||
],
|
||||
"safety": {
|
||||
"queryOnly": true,
|
||||
"readOnlyConnection": true,
|
||||
"forbidMultipleStatements": true,
|
||||
"forbidAttach": true,
|
||||
"forbidWritePragmas": true,
|
||||
"forbidExtensionLoading": true,
|
||||
"maxRows": 16,
|
||||
"timeoutMs": 2000,
|
||||
"maxResultBytes": 32768
|
||||
},
|
||||
"evidence": {
|
||||
"source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
|
||||
"provenJoins": ["bank_account_registry.account_owner_user_profile_id -> user_profile.id", "bank_account_registry_currencies.bank_account_id -> bank_account_registry.id"],
|
||||
"ambiguity": "Currency labels, units, command execution, and readback confirmation are unverified; currencyType remains numeric and version-scoped."
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"key": "scum-player-session-enrichment-read-v1",
|
||||
"adapterVersion": "scum-live-data-v1",
|
||||
"queryVersion": "scum-sqlite-query-v1",
|
||||
"capability": "players.read",
|
||||
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
|
||||
"statementType": "single-select-or-cte",
|
||||
"statement": "SELECT u.id AS externalPlayerId, u.id_type AS idType, u.provider AS provider, u.creation_time AS userCreationTime, u.last_login_time AS userLastLoginTime, u.is_banned AS isBanned, up.id AS profileId, up.name AS displayName, up.type AS profileType, p.id AS prisonerId FROM \"user\" u LEFT JOIN user_profile up ON up.user_id = u.id AND up.type = 1 LEFT JOIN prisoner p ON p.id = up.prisoner_id AND p.user_profile_id = up.id WHERE u.id = :externalPlayerId LIMIT 1",
|
||||
"parameters": [
|
||||
{ "name": "externalPlayerId", "binding": ":externalPlayerId", "type": "string", "required": true, "minLength": 1, "maxLength": 96 }
|
||||
],
|
||||
"safety": {
|
||||
"queryOnly": true,
|
||||
"readOnlyConnection": true,
|
||||
"forbidMultipleStatements": true,
|
||||
"forbidAttach": true,
|
||||
"forbidWritePragmas": true,
|
||||
"forbidExtensionLoading": true,
|
||||
"maxRows": 1,
|
||||
"timeoutMs": 2000,
|
||||
"maxResultBytes": 32768
|
||||
},
|
||||
"evidence": {
|
||||
"source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
|
||||
"sessionBoundary": "Online state and local sessions are created from authenticated login/logout events; this query only enriches a login-created identity with proven database facts."
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"key": "scum-players-read-v1",
|
||||
"adapterVersion": "scum-live-data-v1",
|
||||
"queryVersion": "scum-sqlite-query-v1",
|
||||
"capability": "players.read",
|
||||
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
|
||||
"statementType": "single-select-or-cte",
|
||||
"statement": "WITH player_rows AS (SELECT u.id AS externalPlayerId, u.id_type AS idType, u.provider AS provider, u.creation_time AS userCreationTime, u.last_login_time AS userLastLoginTime, u.is_banned AS isBanned, up.id AS profileId, up.name AS displayName, up.type AS profileType, p.id AS prisonerId, pe.entity_id AS entityId FROM \"user\" u LEFT JOIN user_profile up ON up.user_id = u.id AND up.type = 1 LEFT JOIN prisoner p ON p.id = up.prisoner_id AND p.user_profile_id = up.id LEFT JOIN prisoner_entity pe ON pe.prisoner_id = p.id ORDER BY u.last_login_time DESC, u.id ASC LIMIT :limit OFFSET :offset) SELECT externalPlayerId, idType, provider, userCreationTime, userLastLoginTime, isBanned, profileId, displayName, profileType, prisonerId, entityId FROM player_rows",
|
||||
"parameters": [
|
||||
{ "name": "limit", "binding": ":limit", "type": "integer", "required": true, "minimum": 1, "maximum": 100 },
|
||||
{ "name": "offset", "binding": ":offset", "type": "integer", "required": true, "minimum": 0, "maximum": 1000000 }
|
||||
],
|
||||
"safety": {
|
||||
"queryOnly": true,
|
||||
"readOnlyConnection": true,
|
||||
"forbidMultipleStatements": true,
|
||||
"forbidAttach": true,
|
||||
"forbidWritePragmas": true,
|
||||
"forbidExtensionLoading": true,
|
||||
"maxRows": 100,
|
||||
"timeoutMs": 2000,
|
||||
"maxResultBytes": 65536
|
||||
},
|
||||
"evidence": {
|
||||
"source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
|
||||
"provenJoins": ["user_profile.user_id -> user.id", "user_profile.prisoner_id -> prisoner.id", "prisoner.user_profile_id -> user_profile.id", "prisoner_entity.prisoner_id -> prisoner.id"]
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"key": "scum-positions-read-v1",
|
||||
"adapterVersion": "scum-live-data-v1",
|
||||
"queryVersion": "scum-sqlite-query-v1",
|
||||
"capability": "positions.read",
|
||||
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
|
||||
"statementType": "single-select-or-cte",
|
||||
"statement": "WITH position_rows AS (SELECT 'player' AS subjectType, u.id AS subjectId, u.id AS externalPlayerId, up.id AS profileId, p.id AS prisonerId, pe.entity_id AS entityId, CAST(NULL AS INTEGER) AS vehicleEntityId, CAST(NULL AS INTEGER) AS vehicleAssetId, CAST(NULL AS INTEGER) AS flagElementId, e.location_x AS x, e.location_y AS y, e.location_z AS z, p.last_save_time AS sourceTimeValue, 'prisoner.last_save_time' AS sourceTimeKind FROM \"user\" u INNER JOIN user_profile up ON up.user_id = u.id AND up.type = 1 INNER JOIN prisoner p ON p.id = up.prisoner_id AND p.user_profile_id = up.id INNER JOIN prisoner_entity pe ON pe.prisoner_id = p.id INNER JOIN entity e ON e.id = pe.entity_id WHERE :subjectType IN ('all', 'player') UNION ALL SELECT 'vehicle' AS subjectType, CAST(vs.vehicle_entity_id AS TEXT) AS subjectId, CAST(NULL AS TEXT) AS externalPlayerId, CAST(NULL AS INTEGER) AS profileId, CAST(NULL AS INTEGER) AS prisonerId, ve.entity_id AS entityId, vs.vehicle_entity_id AS vehicleEntityId, vs.vehicle_asset_id AS vehicleAssetId, CAST(NULL AS INTEGER) AS flagElementId, e.location_x AS x, e.location_y AS y, e.location_z AS z, CAST(NULL AS INTEGER) AS sourceTimeValue, CAST(NULL AS TEXT) AS sourceTimeKind FROM vehicle_spawner vs INNER JOIN vehicle_entity ve ON ve.entity_id = vs.vehicle_entity_id INNER JOIN entity e ON e.id = ve.entity_id WHERE :subjectType IN ('all', 'vehicle') UNION ALL SELECT 'flag' AS subjectType, CAST(bef.element_id AS TEXT) AS subjectId, CAST(NULL AS TEXT) AS externalPlayerId, CAST(NULL AS INTEGER) AS profileId, CAST(NULL AS INTEGER) AS prisonerId, CAST(NULL AS INTEGER) AS entityId, CAST(NULL AS INTEGER) AS vehicleEntityId, CAST(NULL AS INTEGER) AS vehicleAssetId, bef.element_id AS flagElementId, be.location_x AS x, be.location_y AS y, be.location_z AS z, CAST(NULL AS INTEGER) AS sourceTimeValue, CAST(NULL AS TEXT) AS sourceTimeKind FROM base_element_flag bef INNER JOIN base_element be ON be.element_id = bef.element_id INNER JOIN base b ON b.id = be.element_id WHERE :subjectType IN ('all', 'flag')) SELECT subjectType, subjectId, externalPlayerId, profileId, prisonerId, entityId, vehicleEntityId, vehicleAssetId, flagElementId, x, y, z, sourceTimeValue, sourceTimeKind FROM position_rows ORDER BY subjectType ASC, subjectId ASC LIMIT :limit OFFSET :offset",
|
||||
"parameters": [
|
||||
{ "name": "subjectType", "binding": ":subjectType", "type": "string", "required": true, "enum": ["all", "player", "vehicle", "flag"] },
|
||||
{ "name": "limit", "binding": ":limit", "type": "integer", "required": true, "minimum": 1, "maximum": 500 },
|
||||
{ "name": "offset", "binding": ":offset", "type": "integer", "required": true, "minimum": 0, "maximum": 1000000 }
|
||||
],
|
||||
"safety": {
|
||||
"queryOnly": true,
|
||||
"readOnlyConnection": true,
|
||||
"forbidMultipleStatements": true,
|
||||
"forbidAttach": true,
|
||||
"forbidWritePragmas": true,
|
||||
"forbidExtensionLoading": true,
|
||||
"maxRows": 500,
|
||||
"timeoutMs": 2000,
|
||||
"maxResultBytes": 262144
|
||||
},
|
||||
"evidence": {
|
||||
"source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
|
||||
"provenJoins": ["user_profile.user_id -> user.id", "user_profile.prisoner_id -> prisoner.id", "prisoner_entity.prisoner_id -> prisoner.id", "vehicle_spawner.vehicle_entity_id -> vehicle_entity.entity_id", "vehicle_entity.entity_id -> entity.id", "base_element_flag.element_id -> base_element.element_id", "base_element.element_id -> base.id"],
|
||||
"cadenceCaveat": "SCUM.db does not prove sub-10s realtime cadence; sourceTimeValue is nullable and only reflects fields proven by the adapter."
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"key": "scum-squad-members-read-v1",
|
||||
"adapterVersion": "scum-live-data-v1",
|
||||
"queryVersion": "scum-sqlite-query-v1",
|
||||
"capability": "squad-members.read",
|
||||
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
|
||||
"statementType": "single-select-or-cte",
|
||||
"statement": "SELECT sm.squad_id AS squadId, sm.user_profile_id AS userProfileId, sm.rank AS rankCode, CAST(NULL AS TEXT) AS rankMeaning, CAST(NULL AS INTEGER) AS isLeader, up.user_id AS externalPlayerId, u.id_type AS idType, u.provider AS provider, up.name AS displayName, up.type AS profileType FROM squad_member sm INNER JOIN squad s ON s.id = sm.squad_id INNER JOIN user_profile up ON up.id = sm.user_profile_id LEFT JOIN \"user\" u ON u.id = up.user_id WHERE (:squadId IS NULL OR sm.squad_id = :squadId) ORDER BY sm.squad_id ASC, sm.user_profile_id ASC LIMIT :limit OFFSET :offset",
|
||||
"parameters": [
|
||||
{ "name": "squadId", "binding": ":squadId", "type": "integer", "required": true, "nullable": true, "minimum": 0, "maximum": 2147483647 },
|
||||
{ "name": "limit", "binding": ":limit", "type": "integer", "required": true, "minimum": 1, "maximum": 500 },
|
||||
{ "name": "offset", "binding": ":offset", "type": "integer", "required": true, "minimum": 0, "maximum": 1000000 }
|
||||
],
|
||||
"safety": {
|
||||
"queryOnly": true,
|
||||
"readOnlyConnection": true,
|
||||
"forbidMultipleStatements": true,
|
||||
"forbidAttach": true,
|
||||
"forbidWritePragmas": true,
|
||||
"forbidExtensionLoading": true,
|
||||
"maxRows": 500,
|
||||
"timeoutMs": 2000,
|
||||
"maxResultBytes": 131072
|
||||
},
|
||||
"evidence": {
|
||||
"source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
|
||||
"provenJoins": ["squad_member.squad_id -> squad.id", "squad_member.user_profile_id -> user_profile.id", "user_profile.user_id -> user.id"],
|
||||
"ambiguousFieldsKeptNull": ["rankMeaning", "isLeader"]
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"key": "scum-squads-read-v1",
|
||||
"adapterVersion": "scum-live-data-v1",
|
||||
"queryVersion": "scum-sqlite-query-v1",
|
||||
"capability": "squads.read",
|
||||
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
|
||||
"statementType": "single-select-or-cte",
|
||||
"statement": "WITH member_counts AS (SELECT sm.squad_id AS squadId, COUNT(*) AS memberCount FROM squad_member sm GROUP BY sm.squad_id) SELECT s.id AS squadId, s.name AS squadName, COALESCE(mc.memberCount, 0) AS memberCount, CAST(NULL AS INTEGER) AS leaderProfileId, CAST(NULL AS TEXT) AS leaderExternalPlayerId, CAST(NULL AS INTEGER) AS leaderRankCode, CAST(NULL AS TEXT) AS leaderRankMeaning FROM squad s LEFT JOIN member_counts mc ON mc.squadId = s.id ORDER BY s.id ASC LIMIT :limit OFFSET :offset",
|
||||
"parameters": [
|
||||
{ "name": "limit", "binding": ":limit", "type": "integer", "required": true, "minimum": 1, "maximum": 100 },
|
||||
{ "name": "offset", "binding": ":offset", "type": "integer", "required": true, "minimum": 0, "maximum": 1000000 }
|
||||
],
|
||||
"safety": {
|
||||
"queryOnly": true,
|
||||
"readOnlyConnection": true,
|
||||
"forbidMultipleStatements": true,
|
||||
"forbidAttach": true,
|
||||
"forbidWritePragmas": true,
|
||||
"forbidExtensionLoading": true,
|
||||
"maxRows": 100,
|
||||
"timeoutMs": 2000,
|
||||
"maxResultBytes": 65536
|
||||
},
|
||||
"evidence": {
|
||||
"source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
|
||||
"provenFields": ["squad.id", "squad.name", "squad_member.squad_id"],
|
||||
"ambiguousFieldsKeptNull": ["leaderProfileId", "leaderExternalPlayerId", "leaderRankCode", "leaderRankMeaning"]
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"key": "scum-vehicles-read-v1",
|
||||
"adapterVersion": "scum-live-data-v1",
|
||||
"queryVersion": "scum-sqlite-query-v1",
|
||||
"capability": "vehicles.read",
|
||||
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
|
||||
"statementType": "single-select-or-cte",
|
||||
"statement": "SELECT vs.vehicle_entity_id AS vehicleEntityId, ve.entity_id AS entityId, vs.vehicle_asset_id AS vehicleAssetId, vs.is_vehicle_functional AS isVehicleFunctional, CAST(NULL AS TEXT) AS vehicleLabel, CAST(NULL AS INTEGER) AS ownerProfileId, CAST(NULL AS TEXT) AS ownerExternalPlayerId, CAST(NULL AS INTEGER) AS ownerSquadId, e.location_x AS x, e.location_y AS y, e.location_z AS z FROM vehicle_spawner vs INNER JOIN vehicle_entity ve ON ve.entity_id = vs.vehicle_entity_id INNER JOIN entity e ON e.id = ve.entity_id ORDER BY vs.vehicle_entity_id ASC LIMIT :limit OFFSET :offset",
|
||||
"parameters": [
|
||||
{ "name": "limit", "binding": ":limit", "type": "integer", "required": true, "minimum": 1, "maximum": 500 },
|
||||
{ "name": "offset", "binding": ":offset", "type": "integer", "required": true, "minimum": 0, "maximum": 1000000 }
|
||||
],
|
||||
"safety": {
|
||||
"queryOnly": true,
|
||||
"readOnlyConnection": true,
|
||||
"forbidMultipleStatements": true,
|
||||
"forbidAttach": true,
|
||||
"forbidWritePragmas": true,
|
||||
"forbidExtensionLoading": true,
|
||||
"maxRows": 500,
|
||||
"timeoutMs": 2000,
|
||||
"maxResultBytes": 262144
|
||||
},
|
||||
"evidence": {
|
||||
"source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
|
||||
"provenJoins": ["vehicle_spawner.vehicle_entity_id -> vehicle_entity.entity_id", "vehicle_entity.entity_id -> entity.id"],
|
||||
"ambiguousFieldsKeptNull": ["vehicleLabel", "ownerProfileId", "ownerExternalPlayerId", "ownerSquadId"]
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ func e2eClaim(id, commandType string, payload map[string]any, stamp time.Time) C
|
||||
}
|
||||
|
||||
func TestSupportedAdaptersDispatchThroughIsolatedTypedPorts(t *testing.T) {
|
||||
stamp := time.Now().UTC()
|
||||
stamp := time.Date(2026, time.July, 29, 12, 0, 0, 0, time.UTC)
|
||||
port := &isolatedAdapterPort{
|
||||
configFields: map[string]string{"ServerName": "Moonlight", "Password": "never-return", "hostPath": "C:/private/server.ini"},
|
||||
notifyAccept: true,
|
||||
@@ -130,7 +130,7 @@ func TestSupportedAdaptersDispatchThroughIsolatedTypedPorts(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSupportedAdaptersFailClosedForBindingApprovalAndCapability(t *testing.T) {
|
||||
stamp := time.Now().UTC()
|
||||
stamp := time.Date(2026, time.July, 29, 12, 0, 0, 0, time.UTC)
|
||||
for name, testCase := range map[string]struct {
|
||||
availability HandlerAvailability
|
||||
adapter RuntimeAdapter
|
||||
@@ -156,7 +156,7 @@ func TestSupportedAdaptersFailClosedForBindingApprovalAndCapability(t *testing.T
|
||||
}
|
||||
|
||||
func TestSupportedAdapterTransportFailuresCompleteWithoutProtectedOutput(t *testing.T) {
|
||||
stamp := time.Now().UTC()
|
||||
stamp := time.Date(2026, time.July, 29, 12, 0, 0, 0, time.UTC)
|
||||
port := &isolatedAdapterPort{patchErr: errors.New("private port failed"), notifyErr: errors.New("private notification failed")}
|
||||
adapter := RuntimeAdapter{BoundServerID: "server-1", Config: port, Notification: port}
|
||||
gateway := &isolatedDispatchGateway{commands: []ClaimedCommand{
|
||||
@@ -170,11 +170,11 @@ func TestSupportedAdapterTransportFailuresCompleteWithoutProtectedOutput(t *test
|
||||
for id, results := range gateway.completed {
|
||||
result := results[0]
|
||||
if result.Payload["result"] != "failed" || strings.Contains(stringifyPayload(result.Payload), "private") || strings.Contains(result.Summary, "private") {
|
||||
t.Fatalf("%s did not redact failed adapter output: %+v", id, result)
|
||||
t.Fatalf("%s did not redact failed typed-port output: %+v", id, result)
|
||||
}
|
||||
}
|
||||
if len(port.notifications) != 1 || port.notifications[0].protectedAuditCommand == "" {
|
||||
t.Fatalf("notification fixture did not receive one protected request: %+v", port.notifications)
|
||||
t.Fatalf("notification fixture did not receive one protected typed request: %+v", port.notifications)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user