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
123 changes: 123 additions & 0 deletions scripts/acceptance/command-keys/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Installed CLI command-key experiments

These manual adapters exercise shared [#216](https://github.com/durable-workflow/.github/issues/216)
with the published CLI 2.8.0 binary and native Server 3.9.2, linux/amd64.
They build no package and use only disposable synthetic SQLite state. Run one
adapter at a time after the normal host-capacity/owner check. Docker runtime
limits are one CPU, 1 GiB and 256 PIDs, with a loopback-only ephemeral port.

The modern adapter covers admission, committed-response loss, explicit retry,
concurrency, Unicode limits, terminal runs and namespace fences. Its typed phase
covers int64, null, Unicode, reordered maps, integer/float and signed-zero intent. Its second
phase restarts the actual Server process on the same database and checks the
previously lost receipts and explicitly selected terminal runs. The legacy
adapter makes current-producer API admissions, stops that Server, transforms a
separate copy through the accepted helper, and checks legacy readback and
ambiguity refusals. **The synthetic transform is not a physical 3.8 upgrade.**
Physical upgrade input and the producer's accepted seed procedure have their
own source/qualification gate. Neither adapter claims another SDK's Worker
acceptance.

Prepare an empty, canonical directory owned by the invoking user, mode `0700`,
on disk-backed scratch. Put the checksum/attestation-verified published binary
at `bin/dw` inside it. Expected binary SHA256:
`da366c79c90978a3c0a92d723c66a96a2d0963f9dccbd41c8207f2c41848f814`.
The binary must be executable. The runtime image is pulled separately, without
registry credentials, using the exact index in `cli_216.py`. Independently
verify the index/platform manifest and source revision before execution; do
not equate an engine's image ID with the raw OCI config digest.

Stage these public, reviewed source files in the experiment directory:

| File | Accepted source | SHA256 |
| --- | --- | --- |
| `committed_response_loss.py` | `.github` commit `117118c83093822469b3824ba9587ff4068ed29f`, `conformance/command-keys/committed_response_loss.py` | `bd1e8ccbbf76f63fc2eafa278a023eb181d405b68d9c30016392e395a1eff725` |
| `sqlite_legacy.py` | `sdk-rust` commit `4add871b175517f2031f71c868ce278bbfbba486`, `scripts/acceptance/command-keys/sqlite_legacy.py` | `6fc6054ce087c136a86d7b6b222daeefaa87c58fa0b613b155a5df39e6a26fad` |
| `command-key-legacy-v1-vectors.json` | `server` commit `d66e33bfb5c76f94c5b202f8c9f7a9e8cd75cf1c`, `resources/platform-protocol-specs/command-key-legacy-v1-vectors.json` | `da1990000c611d7377690c7526487d6415fa4f9cb42d299f59585a7c2eebf769` |

The complete-snapshot helper and manifest are used by both adapters. Its
transform is used only by `legacy_cli.py`. The proxy is shared too. Inputs must
be canonical, owned, single-link regular files, with no group/world write bits.
The runner holds their descriptors, executes/imports sealed or in-memory checked
bytes and rechecks identities after use. Do not use working tree overlays or
symlink/hardlink staging. Container operations use the exact ID returned by the
runner's creation call; an existing or reused foreign name grants no authority.

Prepare the official-codec fixture with a separately verified installed Python
SDK 2.17.1. Its PyPI wheel SHA256 is
`90040cb28906985b227988815b1b53aa3079378673e35c689392ac71edaf7b35`.
Compare installed package files against that wheel and retain module-origin and
dependency evidence. This is codec preparation, not Python Worker qualification.
Write `worker-envelopes.json` in the experiment directory using that verified
interpreter and the following code (no trailing newline):

```python
import hashlib
import importlib.metadata
import json
from pathlib import Path
from durable_workflow import serializer

Path("worker-envelopes.json").write_text(json.dumps({
"empty": serializer.envelope(None, codec="avro"),
"one": serializer.envelope(1, codec="avro"),
"serializer_sha256": hashlib.sha256(Path(serializer.__file__).read_bytes()).hexdigest(),
"sdk_version": importlib.metadata.version("durable-workflow"),
"fastavro_version": importlib.metadata.version("fastavro"),
}))
```

Use official fastavro 1.13.1 for this frozen fixture. Expected file SHA256 is
`68dac0826e251d0450d458224fa3d4d8714aba8e432df7e241909b8c4b877767`.
The runner holds these exact bytes and compares the actual retained Avro result
with this known fixture. It never executes an external codec program or accepts
its self-reported decoded result.

Run source with isolated host Python and the private directory in
`CLI_216_WORKDIR`. Provide a clean environment without cloud, GitHub, Composer
or registry credentials. Runtime authentication is synthetic and private.

```bash
env -i PATH=/usr/bin:/bin \
CLI_216_WORKDIR="$experiment" \
CLI_216_PROXY_SOURCE="$experiment/committed_response_loss.py" \
timeout 180s python3 -I -B scripts/acceptance/command-keys/cli_216.py
```

For the restart phase, preserve the first phase's `admissions.json` as
`modern-admissions.json`, preserve its result/wire records separately, then run
the same command with `CLI_216_PHASE=selected-restart`. Preserve that phase's
records separately too. `CLI_216_PHASE=fresh-selected` adds direct fresh
explicit-run literal-key markers (the loss cells intentionally withhold those
initial response bodies). `CLI_216_PHASE=typed` runs the separate typed-intent
cases. Use a **new** private
directory for `legacy_cli.py`, with the same staged inputs and environment.
The legacy adapter generates its own admissions and stopped-copy database.

An outer experiment owner must also enforce the timeout and remove only the
exact created container ID if the process is interrupted. Names are
`cli-216-392-modern` and `cli-216-392-legacy`; remove their anonymous volumes
too, and verify that ID is absent. Never remove a container by a reused name.
No listener or production service is retained.

Keep generated databases, response bodies and invocation records private and
bounded in retention. Publish sanitized tuple/source hashes, timestamps,
scenario outcomes, snapshot fingerprints, nonpasses and cleanup to #216.
The wire fixture records only metadata and hashes. A script exit alone does
not classify a failed assertion as a product defect: preserve the first result
and diagnose runner/source/cleanup failures explicitly. A later corrected run
does not erase it. The adapters are manual evidence producers, with no workflow
activation, release authority, task scheduler or automatic retry service.

The offline boundary controls use no Docker, network or credentials:

```bash
CLI_216_TEST_HELPER="$experiment/sqlite_legacy.py" \
python3 -I -B scripts/acceptance/command-keys/test_boundaries.py -v
```

They cover executable/module substitution, symlink/hardlink and same-byte inode
replacement, a forged codec, sealed-content writes, foreign-name survival and
schema-only mutations that the predecessor's row-only comparison accepted.
Modern and legacy zero-write checks use the same bounded typed logical snapshot,
including catalog SQL, columns, indexes, triggers, views and all table rows.
123 changes: 123 additions & 0 deletions scripts/acceptance/command-keys/boundaries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""Held inputs and exact container ownership for the manual Linux experiment."""
import fcntl
import hashlib
import json
import os
from pathlib import Path
import re
import stat
import sys
import types


class HeldInput:
def __init__(self, path, expected, *, maximum=16 * 1024 * 1024):
self.path = Path(path)
if not self.path.is_absolute() or self.path.resolve() != self.path:
raise ValueError("input must be an absolute path without symlinks")
self.fd = os.open(self.path, os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC)
self.sealed_fd = None
try:
self.identity = self._identity(os.fstat(self.fd))
value = os.fstat(self.fd)
if (not stat.S_ISREG(value.st_mode) or value.st_uid != os.getuid()
or value.st_nlink != 1 or value.st_mode & 0o022
or not 0 < value.st_size <= maximum):
raise ValueError("input must be an owned single-link non-writable-by-others regular file")
self.data = os.pread(self.fd, maximum + 1, 0)
actual = hashlib.sha256(self.data).hexdigest()
if len(self.data) != value.st_size or (expected is not None and actual != expected):
raise ValueError("input content identity differs")
# None is used only to capture an already-running entrypoint.
# Every external executable/module/fixture has a literal digest.
self.digest = actual
self.recheck()
self.sealed_fd = os.memfd_create("cli-216-input", os.MFD_CLOEXEC | os.MFD_ALLOW_SEALING)
with os.fdopen(os.dup(self.sealed_fd), "wb") as output:
output.write(self.data)
os.fchmod(self.sealed_fd, 0o500)
fcntl.fcntl(self.sealed_fd, fcntl.F_ADD_SEALS,
fcntl.F_SEAL_WRITE | fcntl.F_SEAL_GROW | fcntl.F_SEAL_SHRINK | fcntl.F_SEAL_SEAL)
except BaseException:
self.close()
raise

@staticmethod
def _identity(value):
return (value.st_dev, value.st_ino, value.st_mode, value.st_uid, value.st_gid,
value.st_nlink, value.st_size, value.st_mtime_ns, value.st_ctime_ns)

def recheck(self):
if (self.path.resolve() != self.path or self._identity(self.path.lstat()) != self.identity
or self._identity(os.fstat(self.fd)) != self.identity):
raise ValueError("held input path or descriptor identity changed")
current = os.pread(self.fd, len(self.data) + 1, 0)
if hashlib.sha256(current).hexdigest() != self.digest:
raise ValueError("held input content changed")

def module(self, name):
self.recheck()
module = types.ModuleType(name)
module.__file__ = str(self.path)
sys.modules[name] = module
exec(compile(self.data, str(self.path), "exec"), module.__dict__)
self.recheck()
return module

@property
def executable(self):
return "/proc/self/fd/" + str(self.sealed_fd)

def close(self):
for name in ("fd", "sealed_fd"):
descriptor = getattr(self, name, None)
if descriptor is not None:
os.close(descriptor)
setattr(self, name, None)


def private_directory(path):
path = Path(path)
if not path.is_absolute() or path.resolve() != path:
raise ValueError("private directory must be absolute and have no symlink component")
path.mkdir(mode=0o700, exist_ok=True)
value = path.lstat()
if not stat.S_ISDIR(value.st_mode) or value.st_uid != os.getuid() or stat.S_IMODE(value.st_mode) != 0o700:
raise ValueError("private directory must be owned and mode 0700")
return path


class OwnedContainer:
def __init__(self, docker, name):
self.docker, self.name, self.id = docker, name, None
self.created_ids = []

def run(self, *arguments):
if self.exists() or self.docker("ps", "-aq", "--filter", "name=^" + self.name + "$"):
raise ValueError("container name already exists; no cleanup authority acquired")
created = self.docker("run", *arguments)
if not re.fullmatch(r"[0-9a-f]{64}", created, re.ASCII):
raise ValueError("Docker did not return one canonical created container ID")
self.id = created
self.created_ids.append(created)
return self.inspect()

def exists(self):
return self.id is not None and self.docker("ps", "-aq", "--no-trunc", "--filter", "id=" + self.id) == self.id

def inspect(self):
if not self.exists():
raise ValueError("created container no longer exists")
value = json.loads(self.docker("inspect", self.id, "--format", "{{json .}}"))
if value["Id"] != self.id or value["Name"] != "/" + self.name:
raise ValueError("created container ID/name changed")
return value

def stop(self):
self.inspect()
self.docker("stop", "--time=10", self.id)

def remove(self):
if self.exists():
self.inspect()
self.docker("rm", "-fv", self.id)
Loading
Loading