Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/portable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ jobs:
- run: php tests/windows/constructor.php vendor/autoload.php --negative-control
- run: php tests/windows/constructor.php vendor/autoload.php --cache-negative-control
- run: php tests/windows/portable.php
- run: php tests/windows/hosted-verifier.php
- run: php tests/windows/native-lifecycle.php
- run: php tests/windows/bootstrap.php
- run: php tests/windows/bootstrap.php --negative-control
- run: php tests/windows/process.php
- run: php tests/windows/shell-guard.php
- run: php tests/windows/ready.php
- run: php tests/windows/ready.php vendor/autoload.php --hosted
- run: php tests/windows/ready.php vendor/autoload.php --unconfigured
- run: php -l bot.php
- run: php -l src/Civ13/Civ13.php
Expand Down
2 changes: 1 addition & 1 deletion bot.php
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ function loadEnv(string $filePath, bool $explicit = false): array
$hidden_options['web_address'] = '127.0.0.1';
$hidden_options['http_port'] = $portable['http_port'];
$hidden_options['functions'] = []; // No ambient production callbacks.
$options['verify_url'] = ''; // No authority writes or fallback to the official verifier.
// runtimeOptions permits only an explicit hosted GET consumer, never authority writes.
$options['webserver_url'] = '127.0.0.1';
}
$options = array_merge($options, $hidden_options);
Expand Down
16 changes: 16 additions & 0 deletions config/examples/windows-native-lifecycle.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"enabled": false,
"supervisor_root": "D:/Civ13-local/TEST1714-supervisor",
"supervisor_sha256": "REPLACE_WITH_REVIEWED_SUPERVISOR_SHA256",
"guard_sha256": "REPLACE_WITH_REVIEWED_MASTER_GUARD_SHA256",
"config_sha256": "REPLACE_WITH_REVIEWED_SUPERVISOR_CONFIG_SHA256",
"daemon": "D:/Civ13-local/BYOND/516.1681/bin/dd.exe",
"daemon_sha256": "REPLACE_WITH_REVIEWED_DAEMON_SHA256",
"expected": {
"revision": "REPLACE_WITH_FRESHLY_VERIFIED_MASTER_COMMIT",
"map": "NOMADS_GAIA",
"generation": "REPLACE_WITH_VERIFIED_ROUND_V2_GENERATION",
"dmb_sha256": "REPLACE_WITH_EXACT_MASTER_DMB_SHA256",
"rsc_sha256": "REPLACE_WITH_EXACT_MASTER_RSC_SHA256"
}
}
1 change: 1 addition & 0 deletions config/examples/windows-test1714.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"ip": "127.0.0.1",
"port": 1714,
"enabled": false,
"native_lifecycle": {"enabled": false},
"supported": false,
"moderate": false,
"legacy_relay": false,
Expand Down
58 changes: 58 additions & 0 deletions docs/windows-hosted-verifier.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Portable hosted verification reads

An explicit portable profile may select the hosted Civilizationbot's existing
public projection with:

```json
"verification_source": {
"mode": "read-only",
"url": "http://civ13.com:55555/verified"
}
```

This exact HTTP GET contract comes from the bot's `SocketServer` HTTP listener
and `/verified` route. It is a legacy unsigned public read projection, not a
signed replica or authenticated TLS transport. No authority token is needed or
sent. Do not configure `CIV_TOKEN`, copy production credentials, run a local
VerifierServer, or interpret the Desktop instance as a verification authority.
The separate app's own Discord token remains private and unrelated.

The consumer uses the bot's asynchronous ReactPHP HTTP browser without a proxy,
disables redirects, bounds each request to 10 seconds and response bytes to
4 MiB, and validates at most 50,000 one-to-one
`ss13`/`discord` records from the hosted collection (JSON list or keyed object),
normalizes legacy account display spelling to the bot's existing lookup form,
rejects normalization collisions, and retains only those fields in memory. It reads at
startup and every 30 minutes, replacing the prior view even when the valid list
is empty. Non-200, timeout, malformed, duplicate or oversized responses mark
the source unavailable and retain the last valid list in memory as stale.
Lookups then reject as unavailable; the local `/verified` read endpoint reports
503 with staleness/last-success metadata. No stale disk cache or
provisional registration can become an alternative source. The projection has
no remote revision/expiry; the refresh interval is not proof of instantaneous
remote freshness.

Staff may invoke `/refreshverification` in the configured guild. It uses the
same staff-role/Moderate Members/Administrator/Ban Members check as the staff
panel, rechecks permission after an ephemeral acknowledgement, and reports
fresh/unavailable, last successful refresh UTC and retained record count.
There is one GET plus at most one retry on a transport rejection (20 seconds
total HTTP deadline); HTTP/schema failures are not retried. Concurrent refreshes and
queued bursts within ten seconds are coalesced. Failed refresh never advances
the last-success timestamp; no valid initial list remains unavailable. A view
older than 30 minutes cannot authorize even if the periodic callback is late.

Read-only mode does not register the legacy verifier's member/thread mutation
listeners, approve guild join requests or run verification-driven role/ban
timers. It does not synchronize Discord roles. Registration, token challenges,
provisioning and deletion are rejected before local or remote writes. Direct
users to the primary hosted bot to complete or change verification. Other bot
commands retain their existing authorization checks; this mode grants none.

`php tests/windows/hosted-verifier.php` uses synthetic records and an injected
asynchronous browser around the real setup/parser/write barriers. It proves
unrelated timers progress while refresh is pending. It opens no socket,
loads no environment/token and makes no Discord or hosted service change.
Live deployment additionally needs an exact endpoint read and normal runtime
ownership/activation checks. Never log response bodies or verified identities
as readiness evidence; retain only status, schema and record count.
165 changes: 165 additions & 0 deletions docs/windows-native-lifecycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Native Windows supervisor adapter (disabled by default)

This adds a bot-side adapter and a versioned supervisor spool contract. It does
**not** add a game supervisor consumer, upgrade an installed supervisor, install
dependencies, provision credentials, launch a game or remove a halt latch.
The installed Desktop supervisor does not implement this protocol. Keep the
adapter disabled until a separately reviewed compatible consumer exists.

The native bot entry points retain their default rejection. An explicitly enabled
`servers.<key>.native_lifecycle` object in a portable profile allows `Host` to
request `start`, `Kill` to request **save-stop**, and `Restart` to request
`restart`. The server must also be explicitly enabled and bound to loopback
TEST1714. Native MapSwap and all legacy shell maintenance remain disabled.
Linux without a portable profile retains its existing routing.

The adapter does not execute Python, DreamDaemon, arbitrary argv, shell text,
process-name kills or port-owner termination. It submits fixed private files to
an independently operated supervisor. Discord arguments never select paths,
commands, revisions, maps, saves or PIDs. Existing Discord command authorization
is unchanged; no registration or live interaction is performed by this change.

## Explicit profile binding

The existing `config/examples/windows-test1714.json` retains
`"native_lifecycle": {"enabled": false}`. The separate
`config/examples/windows-native-lifecycle.json` shows the complete object an
operator may later place under that key. Its placeholders deliberately fail
validation if enabled without verified values. Do not reuse another bot's token
or environment. No credentials belong in this object or in spool records.

Activation requires all of the following:

- A separate portable bot profile, private destinations and environment file,
and dependencies in the intended durable bot installation.
- An existing private `<supervisor_root>/bot-control` directory, with no linked
paths and local access restricted by the operator. This PR changes no ACLs.
- Reviewed SHA-256 pins for the installed `software/supervisor.py`,
`software/master_guard.py`, `supervisor.json`, daemon and exact DMB/RSC pair.
- A clean dedicated `master` repository selected by portable `gitdir`, canonical
Civ13 origin, exact master revision, map and completed saved generation.
- An explicit server `basedir` inside `<supervisor_root>/runs` that is also the
actual game working directory. The consumer must preserve that binding for
this operation; a different run directory is rejected. Moving normal file IPC
to a new run requires a separate profile/restart review because GameServer's
existing resolved paths are readonly.
- Supervisor config fields `root`, `repository`, `branch: "master"`,
`bind: "127.0.0.1"`, `port: 1714`, the exact `map` and `daemon`, and
`bot_control_protocol: "civilizationbot.native-supervisor.v1"`.

Changing a pinned source/config/build requires explicit profile review and bot
restart. The adapter never updates a checkout, compiles, selects an old save,
substitutes a fresh round or silently relaxes save validation.

## Supervisor consumer contract

The trusted consumer must be reviewed with the pinned source; advertising a
capability is not an independent audit of that source. The protocol is a local
operator boundary, not a sandbox against another process with write access to
the private directory. A consumer must serialize with all its existing lifecycle
operators, including scheduled/manual updates. It must **not** consume these
requests without the same ownership and save safety guarantees.

The adapter's `description()` returns the complete `binding` and its SHA-256
(UTF-8 JSON, PHP JSON_UNESCAPED_SLASHES, field order as returned). The consumer
publishes `capability.json` atomically with:

```json
{
"protocol": "civilizationbot.native-supervisor.v1",
"binding_sha256": "64 lowercase hex characters",
"heartbeat_unix": 0,
"epoch": "32 lowercase hex characters",
"sequence": 1,
"master_verified": true,
"busy": false,
"process": null
}
```

The heartbeat must be within ten seconds, not more than two seconds in the
future. `epoch` identifies one reconciled supervisor session; `sequence` must
advance after each operation. `process: null` asserts that the supervisor has
established a stopped state with no conflicting owner/listener. A running
process record instead contains `pid` (positive integer), `creation_filetime`
(decimal string), canonical `executable` and `working_directory`, and the bound
`dmb_sha256`/`rsc_sha256`. The consumer must obtain these from the actual process,
not a port label, filename, stale PID or intended command line.

The adapter briefly holds a nonblocking exclusive `adapter.lock`, persists `pending.json`,
then publishes `request.json` using an exclusive hard link to a flushed temporary
file. A filesystem without the required hard-link/fsync behavior fails closed.
The packet contains only protocol, random operation ID, fixed action,
epoch/sequence, binding hash, complete binding, and the observed `before` process.
An existing request/pending record or consumed epoch/sequence refuses submission.
The publication lock is released before waiting; completion reacquires it only
for its short verification/acknowledgement transition. A separate process can
inspect or reconcile state while an operation is pending, but must not replay it.

On accepting an operation the consumer must:

1. Revalidate all pins, protocol, epoch/sequence, expected master/map/save and
actual ownership under its own lifecycle lock. Fetch canonical origin/master,
require clean branch master equal to that verified ref, and fail closed when
offline, stale, dirty or diverged. Never reset, force checkout or stash.
2. For start, refuse an existing/conflicting daemon and verify the selected
completed generation and import intent before startup. For save-stop/restart,
match PID plus creation time, executable, working directory and binary hashes;
refuse busy/error/client/hold conditions. Use only the authenticated existing
save/shutdown API. A lost reply is unknown, not permission to retry or kill.
3. Validate the completed save and acknowledged exit before restart. Start only
the bound binary with loopback binding, then verify actual ownership and
application readiness/imported generation. TCP-only success is insufficient.
4. Persist a single immutable `result-<operation>.json` atomically, consume the
request exactly once, and advance its capability sequence. Recovery must
reconcile durable pending/result/state rather than replay a request.

Completion results must echo protocol, operation, action, epoch, sequence,
binding hash and exact `before`; set `outcome: "complete"`,
`master_verified: true`, `ready` (false for save-stop, true otherwise), and
`after` (null only for save-stop). A running `after` uses the process schema
above. Restart must establish a different creation identity. Save-stop/restart
also require `exit_acknowledged: true`, `save_complete: true`, a valid
`saved_generation` and `save_receipt_sha256`. Start/restart additionally return
the matching `restored_generation`.

The bot accepts these as a pinned supervisor's report, not independent native
acceptance. Real process/socket, actual save receipt, logs, imported world and
readiness evidence remain required before deployment is considered verified.

The bot polls without blocking its event loop. Queueing is never reported as
success. A ten-minute bot timeout does **not** kill the supervisor or daemon.
Timeout, malformed/unbound response, failed/incomplete save, changed pins, or bot
restart leaves the pending record intact and blocks another request. Successful
completion retains the result plus a consumed epoch/sequence marker before
clearing only the exact pending record. No automatic retry or recovery clearing
is provided; an operator must reconcile evidence before changing those records.
Longer game operations can finish independently after the bot timeout.
If save-stop/restart advances the generation, a durable marker blocks subsequent
operations using the original binding. The operator must review the newly saved
generation, update the explicit profile and supervisor capability binding, and
restart the bot before a later start can proceed. Old results and reconciliation
markers remain evidence; the adapter never selects the original save again.

## Current Desktop launch blockers

The discovered installation still hardcodes Europe while the preserved current
save is Gaia; its configured initial save is older Europe. The Gaia generation
lacks a restart intent, and HALTED.json remains present. The old supervisor also
lacks this spool consumer and explicit loopback bind contract. None is repaired
or bypassed by enabling a flag or installing Composer dependencies. The actual
master binary has not been rebuilt/refreshed. Keep launch disabled while the
supervisor contract, exact build, matching save/intent and operator profile are
resolved and validated under the existing native allocator.

## Offline checks

`php tests/windows/native-lifecycle.php [EXPLICIT_VENDOR_AUTOLOAD]` uses inert
temporary files, an injected clock, fake loop, supervisor receipts and message
objects. It covers fixed routing, default rejection, pins, bad capability,
serialization, stale capability replay, process identity, save/exit/import
controls, malformed/unknown outcomes, interrupted publication and bot restart.
Two real inert PHP children additionally compete for one temporary spool; only
one may publish. No supervisor, game, bot
bootstrap, credential load or network runs. Existing portable constructor,
shell-guard, ready and real inert PHP child contracts remain separate checks.
7 changes: 5 additions & 2 deletions docs/windows-test1714.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,11 @@ The HTTP listener binds 127.0.0.1 on the explicit profile port (distinct from171
Portable mode removes ambient callback functions, official verifier server
construction and the official verification URL. Verification integration remains
unavailable until a separate reviewed authoritative read-only contract is supplied.
Native Host/Kill/Restart/MapSwap fail closed in portable/Windows mode before game
messages or timers. This change supplies no game lifecycle replacement.
Native Host/Kill/Restart/MapSwap fail closed by default in portable/Windows mode
before game messages or timers. The separately documented
[native supervisor adapter](windows-native-lifecycle.md) is explicitly disabled
and requires a compatible reviewed supervisor consumer. It supplies no direct
game lifecycle replacement; MapSwap and legacy shell operations remain disabled.

Legacy shell maintenance is also unavailable in Windows/portable mode: chat
updatedeps/pullrepo/ts/ss14 and slash pull/update report unavailable rather than
Expand Down
5 changes: 5 additions & 0 deletions src/Civ13/Civ13.php
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,7 @@ private function resolveOptions(array &$options = []): array
'stats' => null,
'technician_id' => '116927250145869826',
'verify_url' => 'http://valzargaming.com:8080/verified/',
'verification_read_only' => false,
'web_address' => '',
'webapi' => null,
'webserver_url' => 'www.valzargaming.com',
Expand Down Expand Up @@ -583,6 +584,7 @@ private function resolveOptions(array &$options = []): array
->setAllowedTypes('stats', ['null', Stats::class])
->setAllowedTypes('technician_id', 'string')
->setAllowedTypes('verify_url', 'string')
->setAllowedTypes('verification_read_only', 'bool')
->setAllowedTypes('web_address', 'string')
->setAllowedTypes('webapi', ['null', 'resource', HttpServer::class])
->setAllowedTypes('webserver_url', 'string');
Expand Down Expand Up @@ -1407,12 +1409,14 @@ private function declareListeners(): void
});

$this->discord->on('GUILD_JOIN_REQUEST_CREATE', function (GuildJoinRequest $request): void {
if ($this->verifier->readOnly) { return; }
if ($this->verifier->getVerifiedMemberItems()->get('discord', $request->user_id)) {
$request->approve();
}
});

$this->discord->on('GUILD_JOIN_REQUEST_UPDATE', function (GuildJoinRequest $request): void {
if ($this->verifier->readOnly) { return; }
if ($this->verifier->getVerifiedMemberItems()->get('discord', $request->user_id)) {
$request->approve();
}
Expand Down Expand Up @@ -1551,6 +1555,7 @@ public function listbans(Message $message, ?string $key = null): PromiseInterfac
*/
public function bancheckTimer(): TimerInterface|false
{
if (isset($this->verifier) && $this->verifier->readOnly) { return false; }
// We don't want the persistence server to do this function
if (! $this->enabled_gameservers) {
return false;
Expand Down
Loading
Loading