Skip to content

feat: add isolated qpk-vnext N2 filesystem store#257

Closed
Pigbibi wants to merge 2 commits into
mainfrom
codex/qpk-vnext-n2-isolated-store
Closed

feat: add isolated qpk-vnext N2 filesystem store#257
Pigbibi wants to merge 2 commits into
mainfrom
codex/qpk-vnext-n2-isolated-store

Conversation

@Pigbibi

@Pigbibi Pigbibi commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

QPK-N2 isolated vNext local store

Fresh base: origin/main@a06a99f74566ce60ae91fc39317cdfc16558e325 (merged N1 only).

This PR adds a minimal filesystem-only store for the new qpk-vnext/result/v2 namespace. It consumes only the merged N1 ResultContract/wire/key contract and never reads legacy PerformanceStore, legacy indexes/codecs, or fallback paths.

  • explicit root with containment defense and key-derived paths
  • durable-only; ephemeral fails before filesystem side effects
  • N1 encode/decode validation before write
  • atomic write-once create, byte-identical idempotency, conflicts/corruption fail closed
  • exact key reads and explicit domain/profile/timing listing only
  • no implicit latest (deferred because N2 identity has no approved recency field)
  • deterministic canonical JSON; bounded temp cleanup

Validation

  • focused N2: 5 passed
  • full suite: 619 passed, 1 skipped
  • ruff, compileall, diff-check: passed

Non-goals: N3 caller/orchestrator/exporter, legacy compatibility, network/S3, live/broker/account, merge automation.

Co-Authored-By: Codex <noreply@openai.com>
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

🤖 Codex PR Review

🚫 Merge blocked: 2 serious issue(s) found in high-risk files

🚫 Blocking Issues

These issues must be fixed before this PR can be merged:

1. 🔴 [CRITICAL] Security in src/quant_platform_kit/strategy_lifecycle/qpk_vnext_n2_store.py

Root containment is enforced only by a preflight Path.resolve() check, then later filesystem operations (mkdir, mkstemp, read_bytes, link, read_text) are performed through pathname strings. A concurrent actor that can mutate the store tree can swap one of the checked directories for a symlink after _path_for_key() returns, causing put() or get() to follow the new path outside the configured root and read/write arbitrary process-accessible files. (line 101)

Suggestion: Use descriptor-relative operations (openat/mkdirat-style traversal) with O_NOFOLLOW on every path segment, or otherwise hold verified directory file descriptors and operate relative to them so the checked path cannot be replaced between validation and use.

2. 🟠 [HIGH] Reliability in src/quant_platform_kit/strategy_lifecycle/qpk_vnext_n2_store.py

put() fsyncs only the leaf parent directory after mkdir(parents=True, exist_ok=True). If the write creates multiple previously missing directories, a crash after returning success can lose one of the unsynced ancestor directory entries even though the file payload and final directory were fsynced, violating the documented durable-success contract. (line 101)

Suggestion: Track which ancestor directories were newly created and fsync each parent directory in creation order up to the configured root before reporting success.

ℹ️ Other Findings

1. 🟡 [MEDIUM] Logic in src/quant_platform_kit/strategy_lifecycle/qpk_vnext_n2_store.py

list_keys() returns any correctly shaped *.json path under the selector without validating that the file still decodes to a valid ResultContract. A corrupt or manually planted file is therefore advertised as a valid key, and callers only discover the corruption later when get() fails, which breaks the stated fail-closed behavior for corrupted records. (line 168)

Suggestion: Validate each candidate during listing, either by decoding it with decode_wire() and confirming the embedded key matches the path, or by skipping/failing on unreadable or invalid files instead of returning them.


Review by Codex PR Review bot • PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cfa5022298

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

handle.write(payload)
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve write-once semantics during racing creates

When two writers create the same identity key at the same time (for example the same key with a different computed_at, which N1 excludes from identity), both can pass the earlier path.exists() check; this os.replace then overwrites the file created by the other writer and returns created instead of failing as a conflict. That violates the write-once/conflict-fail-closed contract and can silently change persisted result metadata under concurrent orchestrators; the final install needs an exclusive-create pattern rather than a replacing rename.

Useful? React with 👍 / 👎.

for path in base.rglob("*.json"):
try:
key = path.resolve().relative_to(self.root).as_posix()
if f"/{timing}/" in key:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match the timing path segment exactly

When strategy_id or run_id equals a valid timing such as next_open, a next_close record's key still contains /next_open/ before the actual timing segment, so list_keys(..., timing="next_open") returns that next_close key. This mixes result buckets even for valid contracts; compare the fixed timing component of the relative path instead of substring-searching the whole key.

Useful? React with 👍 / 👎.

def list_keys(self, *, domain: str, profile: str, timing: str) -> tuple[str, ...]:
"""List exact selector matches; no implicit latest or legacy scan."""
prefix = f"{NAMESPACE}/{domain}/{profile}/"
base = (self.root / NAMESPACE / domain / profile).resolve()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject traversal selectors before listing

When domain or profile contains .., this resolved base path is only checked to stay under the store root, not under qpk-vnext/result/v2; for example domain="../../../legacy" makes list_keys walk root/legacy/... and return matching JSON paths. That breaks the isolated namespace/no-legacy-scan guarantee for invalid selector input, so the listing selectors should be validated as single key segments or the listed keys should be constrained to the namespace prefix.

Useful? React with 👍 / 👎.

os.fsync(handle.fileno())
os.replace(temporary, path)
temporary = None
return "created"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Sync the directory before reporting a durable create

On POSIX filesystems, when the process or host crashes after this rename, fsyncing only the file data does not guarantee the new directory entry is durable. Because put() returns created immediately afterward for a persist_mode="durable" contract, callers can believe a result was safely committed even though the key may disappear after crash recovery; fsync the parent directory after the rename before returning success.

Useful? React with 👍 / 👎.

Co-Authored-By: Codex <noreply@openai.com>
@Pigbibi

Pigbibi commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Closure batch (head 94c46a0) addresses all current findings: create-only os.link prevents concurrent overwrite and compares bytes on FileExistsError; selector domain/profile/timing are validated as exact safe segments and candidate timing is positional; file and parent directory metadata are fsynced; adversarial, concurrent, and cleanup regressions added. No legacy/network/latest behavior introduced.

@Pigbibi

Pigbibi commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

PERMANENT_FREEZE: closure exhausted; fresh N2 reslice design required for descriptor-relative containment and full ancestor durability. Frozen evidence retained.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 94c46a08f0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +94 to +95
path.parent.mkdir(parents=True, exist_ok=True)
self._sync_directory(path.parent)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fsync parents created by mkdir before returning

When the store is first used for a new root/domain/profile, mkdir(parents=True) creates several directory entries but this only fsyncs the leaf path.parent; the entries for ancestors such as qpk-vnext, result, or v2 live in their own parent directories and can be lost on a POSIX crash even after put() returns created for persist_mode="durable". Fresh evidence: the updated code now fsyncs after the create-only link, but it still never fsyncs the parents whose directory entries were created by this mkdir call.

Useful? React with 👍 / 👎.

Comment on lines +161 to +163
if (len(parts) == 11 and key.startswith(prefix)
and parts[7] == timing and self._path_for_key(key) == path.resolve()):
found.append(key)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate listed keys before returning them

If any stray or corrupt *.json file exists under a selector path, this predicate returns it as a key as long as the path has 11 segments and the timing segment matches; it does not verify the N1 key shape/digest or decode the wire record. For example qpk-vnext/result/v2/us_equity/SOXL/bad!/run/next_open/oops/p1/notadigest.json is listed even though get() will fail, so callers using list_keys() can receive unusable non-contract keys from the isolated namespace.

Useful? React with 👍 / 👎.

Comment on lines +110 to +111
if existing == payload:
return "idempotent"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wait for directory durability before idempotent races return

When two writers race, the loser can hit this FileExistsError path after the winner has linked the destination but before the winner reaches the directory fsync below; returning idempotent here lets a durable put() report success even though no caller has yet made the new directory entry crash-durable. This branch should fsync the parent (or otherwise wait for the winner's durable commit) before returning success.

Useful? React with 👍 / 👎.

Comment on lines +84 to +85
payload = self._bytes(contract)
path = self._path_for_key(contract.key)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Store bytes at the decoded contract key

Because put() accepts any ResultContract subclass, a subclass that overrides key can pass the N1 encode/decode validation here while causing the canonical payload to be written under a different path than the payload's own identity key. In that case put() returns created, but get(contract.key) fails because the on-disk wire decodes to a contract whose key does not match the path; derive the path from the decoded contract or explicitly compare checked.key to the requested key before writing.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant