← Về thư mục
📄 / / profiles / r-and-d / raw_evidence / OpenAI4S / openai4s / security / README.md

Security layers

中文说明

Six of the layers around Code-as-Action live in this package — code classification, kernel OS confinement, the CPython audit hook, the shell precheck, prompt-injection annotation, and biosecurity screening — and beside them two boundaries that are not about a Cell at all: the OS confinement around the BYOC provider helper, deliberately shaped in the opposite direction to the kernel's, and credential storage, which is where a secret goes once it is out of the database. The other layers sit elsewhere on purpose. Child-environment filtering is in kernel/environment.py, Host permissions and durable approval in host_dispatch.py and storage/permissions.py, the shell capability itself in host/bash.py and sdk/bash.py, application egress in egress.py. There is no single boundary to point at, and that is the design: each control catches something different and fails in its own way, so none of them is the one that has to hold.

Where this fits

Files

File Responsibility
wsl.py WSL detection, Windows mount boundaries, Linux tool PATH and the Hyper-V socket filter; no global WSL changes.
windows_dpapi.py Host-only JSON/stdin bridge to Windows user-scoped DPAPI storage, partitioned by WSL installation/user and broker reference.
__init__.py Documents the layered model and re-exports the code-classification, injection, and biosecurity verdict APIs.
audit_hook.py Installs a CPython audit hook inside the worker: a ctypes.dlopen of a shared library under a writable workspace, scratch, or Artifact root is refused, while loads from the interpreter and package prefixes still go through. It captures the functions it depends on as def-time keyword defaults and drops every Python-level handle to itself once installed, so rebinding names in the module namespace from a Cell does not disarm the check. That is resistance, not immunity: the guard is meant to be hard to defeat from inside a Cell, not impossible.
biosecurity.py Holds the calibrated-accountability prompt and the trajectory screener. A cheap relevance trigger decides whether a screen is worth a model call at all; the call itself is independent, returns ALLOW, ESCALATE, or BLOCK, and is parsed loosely enough to survive a sloppy answer.
byoc_confinement.py The OS boundary around the BYOC provider helper, which is shaped differently from the kernel's on purpose: the helper must reach the network — calling a provider's REST API is its whole job — and must not read the user's home, where a Cell legitimately may. On macOS that is a Seatbelt profile denying the whole read class over $HOME, with the interpreter's own path components named back as file-read-metadata literals so the loader can still resolve the binary, plus a separate file-read-xattr deny that an operation-specific rule keeps alive through the later whole-class allow over the read roots — on macOS an extended attribute routinely is the file. It also denies the keychain services, because the credential is not a file: security find-generic-password asks securityd, in a process the filesystem rules do not cover. On Linux it is --tmpfs $HOME under bubblewrap with the interpreter's own paths bound back, a private PID namespace so /proc cannot be traversed to the daemon's root, and a tmpfs over /run to hide the container control sockets a read-only bind would still let the helper connect(2) to — with the resolver config rebound so DNS survives the mask. available() establishes the boundary and tests it rather than reporting that a binary is installed: on a host with user namespaces disabled, bwrap is present and confines nothing. Network isolation is a separate capability and is not enabled, which confinement_status() says out loud rather than leaving implicit.
classifier.py Classifies one Cell in three tiers: a fast path for code that touches no risk token, high-confidence static attack signatures, and, in llm mode, a model review of whatever is left. It returns structured SAFE/UNSAFE evidence and never raises.
injection.py Scans untrusted tool-returned text with static markers and, optionally, an LLM. Suspected content gets a warning prefix and keeps its payload; nothing is deleted.
permissions.py Owner-only modes for the data directory, the credential database and the sidecars SQLite creates beside it. The database was created at the process umask — typically readable by every local account — which on a shared workstation or a lab server put the whole credential store one cat away. Best-effort by contract: a filesystem that cannot represent POSIX modes must not stop the daemon from starting, so callers get a boolean and report the posture instead of assuming it. A mode is not encryption and does not make plaintext storage acceptable; it removes the cheapest read, and it is what keeps a backup or an rsync of the data directory from being world-readable at the far end.
fsprobe.py os.lstat-backed lstat_is_symlink / lstat_kind for guards that must refuse what they cannot read. CPython 3.14 moved Path.is_symlink(), is_dir(), is_file() and exists() onto os.path, which swallows every OSError, so a try: p.is_symlink() except OSError: refuse guard stops refusing on the container's interpreter and reports an unreadable path as "not a symlink". These probes answer absence plainly and let every other error propagate; the secret-alias walk in host/files.py, the environment-generation layout check, dynamic-scope removal, stale-artifact cleanup, the workspace snapshot and the bring-up verifier read through them.
sandbox.py Wraps the worker command in Seatbelt (macOS) or bubblewrap (Linux). Builds the workspace, private-temp, secret-read and network policy, proves it with a real deny/allow self-test, and reports back a status whose state is enabled, disabled, or unavailable. A team policy also masks OpenAI4S data, other users' personal data-root areas and stale system-temp kernels, then exposes only exact session inputs; it refuses unaccounted pre-existing workspace hardlinks. Under enforced bubblewrap it also owns interrupt delivery: SIGINT travels through a pidfd-pinned worker identity (team mode pins it from bubblewrap's --info-fd report) rather than a racy numeric PID, and a stop that is structurally unreachable is dropped with its reason recorded for take_interrupt_gap(), so Kernel.interrupt() can report an undelivered stop instead of claiming one. Detection and execution are injectable, so the supported paths stay testable from inside a parent sandbox that forbids nesting.
secret_broker.py Credential storage behind an opaque, canonical-data-directory namespace, so a business row holds a pointer and two Stores cannot collide in one system keychain. Moving/copying a data directory or finding an ownerless v1 system-store reference requires entering the credential again; the old global environment-variable contract remains an explicit operator fallback. Stdlib only, so system stores use security on macOS and secret-tool on Linux, both fed on stdin because argv is visible to local ps. Environment injection is read-only and writes nothing to disk. OPENAI4S_SECRET_STORE=auto fails closed rather than silently degrading to plaintext, and there is deliberately no obfuscated-file backend.
secret_migration.py Moving plaintext credentials out of the database, recoverably: write to the new store, verify by reading it back, replace the row with a reference, and only then delete the old value. Every prefix of that order is safe to be interrupted at. The read-back is the load-bearing step — a keychain can accept a write and return nothing when it is locked or a prompt was denied, and deleting the plaintext on the strength of "the write did not raise" is how a security improvement locks a user out of their own model configuration. Nothing here logs a secret; progress is reported as references and a short hash prefix.
shellcheck.py A small static blocklist that runs before kernel-local host.bash. It catches a handful of unambiguously catastrophic literal commands and nothing subtler; parsing shell properly is not something it attempts, by design.

Enforcement and failure matrix

Layer What happens on a positive finding Important failure behavior
Code classifier A static or LLM UNSAFE keeps that Cell from executing. off disables it. Classifier exceptions and an unconfigured LLM fail open, while an unparseable response from an LLM that was actually invoked errs UNSAFE. Heuristic mode allows code that carries a risk token but matches no static attack signature.
OS sandbox An enforced backend confines writes to the workspace and a private temp dir, masks reads of secret files, and blocks raw network unless it is explicitly allowed. enforce fails closed. For a single-user worker, auto logs and may run without OS confinement after a visible failure. A team read policy never degrades: auto/off, missing backend, failed self-test, ambiguous hardlink inventory, or unsafe path composition all refuse the Cell.
CPython audit hook Refuses covered writable-path ctypes.dlopen events. It is Python- and event-specific, not a general native-code or R sandbox, and the trusted-prefix policy has to stay correct for it to mean anything.
Shell precheck Rejects a few unambiguous destructive command strings. It is regex-based, fails open on its own errors, and is explicitly not an obfuscation-resistant sandbox. Host capability and OS confinement remain necessary.
Injection scan Adds a warning banner to the content the model reads. It does not drop or block content. After the static scan, errors, missing model configuration, and unparseable LLM output all fail open.
Biosecurity screen Returns ALLOW, ESCALATE, or BLOCK for the caller's policy to act on. It runs only after a keyword trigger and fails open to ALLOW when unavailable. A verdict alone is not execution isolation.
BYOC helper boundary The provider helper runs with the user's home unreadable and the keychain denied, and verifies that from inside before it reads a credential — exiting 71 without acting if it does not hold. enforce refuses every byoc op where no boundary can be established, not only at submit; auto runs the unconfined form and reports it. The network is never isolated. Two residuals are stated rather than implied: Seatbelt answers a denied path EPERM and a missing one ENOENT, so existence under $HOME is still observable, and extended-attribute names remain readable along the interpreter's own traversal route.
Secret store A credential lives in the system keychain or comes from the process environment, and the database row holds only an opaque reference. auto fails closed when neither backend is available rather than degrading to plaintext. plaintext still exists but is never implicit. Migration is interruptible at every prefix because it verifies by reading back before it deletes anything.

Operational cautions