From 807125720f91f031f21009f01418b21fc4e3329b Mon Sep 17 00:00:00 2001 From: Renato Date: Thu, 13 Aug 2026 11:12:56 +0200 Subject: [PATCH] fix(codex): add native plugin control plane --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- CHANGELOG.md | 15 ++ CITATION.cff | 2 +- PRIVACY.md | 9 +- README.md | 22 +-- SUPPORT.md | 4 +- codemeta.json | 2 +- docs/integrations/codex.md | 43 +++--- docs/operations/codex-plugin-submission.md | 14 +- docs/operations/codex-plugin-test-cases.json | 4 +- .../codex-plugin-smoke-2026-08-13-v0.3.2.json | 16 +++ plugins/marginal/.codex-plugin/plugin.json | 2 +- plugins/marginal/runtime/marginal_runtime.pyz | Bin 396731 -> 399633 bytes plugins/marginal/runtime/provenance.json | 2 +- plugins/marginal/scripts/marginal_control.py | 67 +++++++++ plugins/marginal/skills/marginal/SKILL.md | 43 ++++-- pyproject.toml | 2 +- scripts/build_codex_plugin.py | 14 +- scripts/smoke_codex_plugin.py | 23 ++- site/privacy.html | 3 +- site/support.html | 3 +- src/marginal/__init__.py | 2 +- src/marginal/integrations/codex/commands.py | 65 +++++++++ src/marginal/integrations/codex/identity.py | 2 +- src/marginal/integrations/codex/service.py | 5 +- tests/integrations/codex/test_commands.py | 122 ++++++++++++++++ .../codex/test_marketplace_smoke.py | 2 + tests/plugin/test_codex_plugin.py | 136 +++++++++++++++++- tests/test_public_api_v2.py | 2 +- tests/test_repository_consistency_v2.py | 2 +- 30 files changed, 551 insertions(+), 79 deletions(-) create mode 100644 docs/operations/evidence/codex-plugin-smoke-2026-08-13-v0.3.2.json create mode 100644 plugins/marginal/scripts/marginal_control.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a4b0076..6ea90da 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -10,7 +10,7 @@ body: id: version attributes: label: MARGINAL version - placeholder: "0.3.1" + placeholder: "0.3.2" validations: required: true - type: input diff --git a/CHANGELOG.md b/CHANGELOG.md index 50be3a4..2a22d2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to MARGINAL are documented here. The project follows Semanti ## [Unreleased] +## [0.3.2] - 2026-08-13 + +### Added + +- a dependency-free native control launcher inside the Codex plugin bundle; +- repository-scoped hook attestation fields in `status`, including live authenticated services, + observed lifecycle evidence, and action coverage. + +### Fixed + +- the bundled skill no longer requires a separately installed `marginal` executable or Python + package to inspect and manage the native plugin; +- native management commands now read the same Codex plugin data store used by lifecycle hooks, + avoiding split-brain Shadow Mode reports. + ## [0.3.1] - 2026-08-13 ### Added diff --git a/CITATION.cff b/CITATION.cff index b8020b3..143fda5 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -4,7 +4,7 @@ title: "MARGINAL: Economically Disciplined Compute Allocation for AI Agents" type: software authors: - name: SignalLayer Labs -version: 0.3.1 +version: 0.3.2 date-released: 2026-08-13 license: Apache-2.0 repository-code: "https://github.com/SignalLayerLabs/Marginal" diff --git a/PRIVACY.md b/PRIVACY.md index 13e9b87..a6da618 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -25,11 +25,10 @@ to an issue is an explicit user action; inspect exports before sharing them. ## User controls -- `marginal codex status` shows the local mode. -- `marginal codex demote` returns enforcement to Shadow Mode. -- `marginal uninstall codex` removes the plugin and preserves evidence. -- `marginal uninstall codex --purge-data --yes` removes plugin data explicitly. +- `$marginal` in Codex uses the bundled native control plane to show status or demote. +- `codex plugin remove marginal@marginal` removes the plugin and preserves evidence. +- the optional Python package command `marginal uninstall codex --purge-data --yes` removes plugin + data explicitly. Security issues must follow [SECURITY.md](SECURITY.md). Privacy questions can be filed through the private contact route described in [SUPPORT.md](SUPPORT.md). - diff --git a/README.md b/README.md index 5b73112..f60de20 100644 --- a/README.md +++ b/README.md @@ -212,19 +212,21 @@ codex plugin marketplace add SignalLayerLabs/Marginal --ref main && codex plugin ``` Then open `/hooks` in Codex, review the exact commands, and grant trust only after inspection. -MARGINAL never bypasses the hook trust boundary. Useful management commands: +MARGINAL never bypasses the hook trust boundary. No Python package or global executable is needed. +In Codex, ask the bundled skill directly: + +```text +Use $marginal to report whether a live hook service is active, whether hooks were observed, and whether this repository is in Shadow Mode or Tool Enforcement. +``` + +The skill resolves the installed plugin with `codex plugin list --json` and runs its bundled +`scripts/marginal_control.py`, which reads the same private data store as the hooks. Remove it with: ```bash -marginal codex status -marginal codex doctor -marginal codex review -marginal codex review --candidate ACTION_HASH --verdict waste -marginal codex promote -marginal codex demote -marginal uninstall codex +codex plugin remove marginal@marginal ``` -The Python package can perform the same native installation transaction: +An installed Python package can also perform the native installation transaction: ```bash marginal install codex @@ -235,7 +237,7 @@ marginal install codex Current tagged library install target: ```bash -pip install "marginal-ai @ git+https://github.com/SignalLayerLabs/Marginal.git@v0.3.1" +pip install "marginal-ai @ git+https://github.com/SignalLayerLabs/Marginal.git@v0.3.2" ``` Development checkout: diff --git a/SUPPORT.md b/SUPPORT.md index 058d346..3486187 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -9,8 +9,8 @@ a public issue. MARGINAL is an early open-source reference implementation. Community support is best effort; no service-level agreement is provided. -For Codex integration reports, include the redacted output of `marginal codex doctor`, the Codex -version, operating system, plugin version, and whether `/hooks` shows the expected lifecycle hooks. +For Codex integration reports, ask `$marginal` for the redacted native `doctor` output and include +the Codex version, operating system, plugin version, and whether `/hooks` shows the expected hooks. Never attach `auth.json`, prompts, source code, raw commands, raw tool output, transcripts, access tokens, or the contents of `PLUGIN_DATA` connection files. diff --git a/codemeta.json b/codemeta.json index ec8015b..796b7f9 100644 --- a/codemeta.json +++ b/codemeta.json @@ -6,7 +6,7 @@ "codeRepository": "https://github.com/SignalLayerLabs/Marginal", "issueTracker": "https://github.com/SignalLayerLabs/Marginal/issues", "license": "https://spdx.org/licenses/Apache-2.0", - "version": "0.3.1", + "version": "0.3.2", "datePublished": "2026-08-06", "programmingLanguage": "Python", "runtimePlatform": "Python 3.10-3.13", diff --git a/docs/integrations/codex.md b/docs/integrations/codex.md index 46868cf..7f57b5a 100644 --- a/docs/integrations/codex.md +++ b/docs/integrations/codex.md @@ -12,7 +12,7 @@ codex plugin marketplace add SignalLayerLabs/Marginal --ref main && codex plugin Open `/hooks` in Codex and inspect the four MARGINAL lifecycle commands before granting trust. The plugin never uses the bypass-trust flag. Until trust and runtime coverage are observed, MARGINAL is -inactive or Shadow-only. +unobserved or Shadow-only; lack of evidence alone does not prove that hooks are disabled. An installed Python package can perform the same native transaction: @@ -26,17 +26,8 @@ marginal install codex codex plugin remove marginal@marginal ``` -Or: - -```bash -marginal uninstall codex -``` - -Removal preserves local evidence. Purge it only through the explicit destructive form: - -```bash -marginal uninstall codex --purge-data --yes -``` +Removal preserves local evidence. The optional Python package also exposes +`marginal uninstall codex --purge-data --yes` for an explicit data purge. ## Earned Enforcement @@ -49,7 +40,7 @@ Global installation never turns blocking on. A repository can enter Tool Enforce - p95 decision latency no greater than 75 ms; - an unchanged repository, Codex, plugin, adapter, policy, and hook identity; - an observable outcome contract for every enforced action family; -- explicit `marginal codex promote` intent. +- explicit native `promote` intent. Any identity drift, lifecycle failure, coverage loss, false stop, or unknown enforced outcome invalidates the receipt and demotes to Shadow Mode. Integration failure fails open because MARGINAL @@ -57,23 +48,33 @@ is an efficiency governor, not a security boundary. Failures and false stops rem audit history, then open a fresh evidence window; enforcement can be earned again only with a new 100-action, five-session clean window. -## Commands +## Native commands + +The plugin bundle does not modify `PATH`. Run `codex plugin list --json`, select the exact +`marginal@marginal` entry, and use its `source.path` as ``. Then invoke: | Command | Purpose | | --- | --- | -| `marginal codex status` | Show mode and capability label | -| `marginal codex doctor` | Inspect Codex version, stable hooks, and plugins | -| `marginal codex review` | List redacted, unreviewed stop candidates | -| `marginal codex promote` | Require a ready, hash-valid local receipt | -| `marginal codex demote` | Immediately return to Shadow Mode | +| `python3 /scripts/marginal_control.py status --workspace --json` | Show live hook service, prior evidence, mode, and coverage | +| `python3 /scripts/marginal_control.py doctor --json` | Inspect Codex version, stable hooks, and plugins | +| `python3 /scripts/marginal_control.py review --workspace --json` | List redacted, unreviewed stop candidates | +| `python3 /scripts/marginal_control.py promote --workspace --json` | Require a ready, hash-valid local receipt | +| `python3 /scripts/marginal_control.py demote --workspace --json` | Immediately return to Shadow Mode | Label a candidate by hash; no raw command or output is displayed or persisted: ```bash -marginal codex review --candidate ACTION_HASH --verdict waste -marginal codex review --candidate ACTION_HASH --verdict helpful +python3 /scripts/marginal_control.py review --workspace --candidate ACTION_HASH --verdict waste +python3 /scripts/marginal_control.py review --workspace --candidate ACTION_HASH --verdict helpful ``` +On Windows, replace `python3` with `py -3` and use Windows path separators. From a Codex chat, +`$marginal` performs plugin discovery and these commands automatically. + +`hooks_active` attests at least one live authenticated MARGINAL service for the selected repository; +`hooks_observed` reports persisted lifecycle evidence. Neither field is the enforcement mode, and +the privacy-preserving control plane does not expose a raw Codex chat identifier. + `waste` means the repeated action added no useful evidence. `helpful` marks the recommendation as a false stop and immediately demotes any active receipt. diff --git a/docs/operations/codex-plugin-submission.md b/docs/operations/codex-plugin-submission.md index 232c3af..7b998b4 100644 --- a/docs/operations/codex-plugin-submission.md +++ b/docs/operations/codex-plugin-submission.md @@ -3,7 +3,7 @@ ```text status: not_submitted status_date: 2026-08-13 -plugin_version: 0.3.1 +plugin_version: 0.3.2 marketplace_selector: marginal@marginal portal_submission_type: skills_only_zip portal_access: authentication_required @@ -32,9 +32,9 @@ and permits repository Tool Enforcement only after a versioned evidence gate pro reviewed false stops, and governance overhead. It never reads prompts, source, raw commands, raw outputs, transcripts, or Codex credentials for evidence. -**Release notes:** First universal-directory candidate. Adds MARGINAL's local-first Shadow Mode, -review workflow, Earned Enforcement gates, fail-open demotion, native Codex lifecycle package, -production logo, eight reviewer cases, and reproducible v0.3.1 archive. +**Release notes:** Adds a dependency-free native control plane, repository-scoped hook attestation, +local Shadow Mode, review workflow, Earned Enforcement gates, fail-open demotion, production logo, +eight reviewer cases, and a reproducible v0.3.2 archive. Starter prompts are the three `interface.defaultPrompt` entries in `plugins/marginal/.codex-plugin/plugin.json`. The production logo is @@ -67,10 +67,10 @@ coverage or identity changes. - [x] Secret-marker scan returns zero persisted occurrences. - [x] Reproducible smoke evidence is committed with runtime SHA-256 provenance. - [x] Privacy, terms, support, and eight reviewer cases exist. -- [x] Canonical main contains v0.3.1 after merge and every public Pages URL resolves. +- [ ] Canonical main contains v0.3.2 after merge and every public Pages URL resolves. - [x] A deterministic single-root ZIP builder covers the manifest, skill, hooks, runtime, and logo. - [ ] SignalLayer Labs identity and Apps Management write permission are confirmed in Platform. -- [ ] The v0.3.1 archive is uploaded in Platform and external review is started. +- [ ] The v0.3.2 archive is uploaded in Platform and external review is started. Build the exact upload artifact with: @@ -89,4 +89,4 @@ verified SignalLayer Labs identity; none can be inferred from repository or GitH external identifier, credential, or reviewer correspondence belongs in this repository. Acceptance evidence: -[`codex-plugin-smoke-2026-08-13-v0.3.1.json`](evidence/codex-plugin-smoke-2026-08-13-v0.3.1.json). +[`codex-plugin-smoke-2026-08-13-v0.3.2.json`](evidence/codex-plugin-smoke-2026-08-13-v0.3.2.json). diff --git a/docs/operations/codex-plugin-test-cases.json b/docs/operations/codex-plugin-test-cases.json index 65246d7..5f84509 100644 --- a/docs/operations/codex-plugin-test-cases.json +++ b/docs/operations/codex-plugin-test-cases.json @@ -1,11 +1,11 @@ { "schema_version": 1, - "plugin_version": "0.3.1", + "plugin_version": "0.3.2", "positive": [ { "id": "positive-shadow-status", "prompt": "Use MARGINAL to inspect the current Codex governance mode.", - "expected": "Reports Shadow Mode and Tool Enforcement capability without claiming blocking is active." + "expected": "Uses the bundled native launcher, reports hook observation separately from Shadow Mode, and does not require a global marginal executable." }, { "id": "positive-doctor", diff --git a/docs/operations/evidence/codex-plugin-smoke-2026-08-13-v0.3.2.json b/docs/operations/evidence/codex-plugin-smoke-2026-08-13-v0.3.2.json new file mode 100644 index 0000000..aa31171 --- /dev/null +++ b/docs/operations/evidence/codex-plugin-smoke-2026-08-13-v0.3.2.json @@ -0,0 +1,16 @@ +{ + "codex_version": "codex-cli 0.147.0", + "completed_sessions": 1, + "directory_archive_sha256": "c45bb71d78d501e3159939de1db5c205ee380943a97c809499f61fad8a8fae8d", + "evidence_records": 4, + "hook_coverage": 1.0, + "installed": true, + "marketplace_selector": "marginal@marginal", + "native_control_mode": "shadow", + "native_control_observed": true, + "plugin_version": "0.3.2", + "raw_secret_occurrences": 0, + "removed": true, + "runtime_sha256": "7ea67b6a17d8f44c03b70e29ec29497a00edd0f9c4262e59cb9082f2015eafe6", + "shadow_block_count": 0 +} diff --git a/plugins/marginal/.codex-plugin/plugin.json b/plugins/marginal/.codex-plugin/plugin.json index 8eeb77a..d1c601a 100644 --- a/plugins/marginal/.codex-plugin/plugin.json +++ b/plugins/marginal/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "marginal", - "version": "0.3.1", + "version": "0.3.2", "description": "Evidence-gated compute governance for Codex, local-first and Shadow Mode by default.", "author": { "name": "SignalLayer Labs", diff --git a/plugins/marginal/runtime/marginal_runtime.pyz b/plugins/marginal/runtime/marginal_runtime.pyz index 3e065f2683df935751a8d7685dc6663c7d358d88..0dafd6c1df9d3c6d9773862f3e719c353539c8a2 100644 GIT binary patch delta 2822 zcmaJ@drTb18NZohhhyP*6tF3evTS+W;p2E19P4Zd;5uu}gLr zYFazkY8t1Fn$*4~pDZg4G_KUhRpO9JCpoE`Ka&2_C<<+*`VqUS)JRU7Mvg2+avDLu z-Mxc@qV8#SXTJCM`@WgEH_ug^p0Ai0__jBv%;P?F1MmLrcTatc3wz|_P?Yhgswql% zVp<)a&<#s6r%h2*N2LVU0yqyxjYCsGsYhy;qFUVMI+O$LBT`+rB)7Df+?e8WM3nFu zHS}?595qxenh3`ds-zaXR&6h$Ba<8`+EBb=CNdHXisI>DgoyA`KMnc6^!@3rp+b+x z9%b`D&xM`|8|j<%P-^LsOv{jtLE|sIwOx{K zNH#ZYT2U(y#E)XC-{%PFdJ(y9BgwkHa`0`Z&2XZPik(=Gn67C&W??GfG2OHr^xHe` z3Af>|UACRr70Z%>{1MuaZ0;+7KL%(mJXTAai$jv7j&ps|>TU~nbOlAYwVk#)G7H@_ z(7KY3G8Q$*FnNQUlL;$)h+6@94Ob3AK@Z(DV{mKI&`>gig{zAptEQ@9(proM)^I~M zKtYKoOfK!!H7<%`oR3OM6ca>z$8`Nj24pi3k6KYBt{N`9A*nf(?F@e3P77zO$%zC% zplVh~!q! z+l189y>7bH@kZ*~v;sRu^?!Re2n!Xo+tCtNw4nEdx4hWd zKv~V6Z2QNCt!}QHaDO;Z*v@$Z!%ZiaB`$T_eU#~rACHk_*4+BzejGnh0~( zNG-f`jr4-=ItfD8by8x}d)MpVbUK-SMqpxGov=*1B5UQ7L@R}3AzA|exk@@<=2Idz zxf!Mr!~E9SB6yax!Oz;sJS>$GL3Fndo;Dml_Y39l!3`mA7ar0QmS>eUXaC6=@Ur+B zlDk7?+#AafCwt$igSUT7bG{+3#$Jx3jl0V5k)M`TrDfcpmnEDfEXbs+X}xq%ZY>%9 zT}n2#I(_j!K%)5i3|KhjEr;7TysX?t-EJI|wU0@61q%y8d3{Wu;s%d9wa_(N!kiq( zOKdv4!b$dhR%&CLTY|FDg9*MO4KKb+DwFqpR_O5I(q&Qv?_Va>@aa7`dv7cGI&7<= zBFs+FT)4fBY)GB%AT|BsgD1`=kNQ2HqkiY=f{R|3pZeG$TL|e$RZo!}&-nlF?5$rv z-RJSxqY=nHQk!~)(3cq$|AZ966Hj{?GbS}W(c$b(p$76gNCYnaUC2AN%SUl|apa_S z`GoQ>@|N#?7+nc_61LNu7n61Gl|8?kby4#TX9{d9IrIDnY&T6^u?fljSMs5oux4@x z#t7>tXW;^2-=Zh$;d8?FP}X37E1+neFnE-*s_K>d-Q#Im73XYImMmWT`EZ%CX8P2g zEV9qbHq#H4EQ+)b1xLNCxOxTtwU%F)aTS)lEEld@_`l>$FIpb_=`wjcz4NOqqX%+W zgj(}1<+U8PjV`>k%>Ei|5Kw#gpUV^mj|r@@W>s-lrFMS-g+9t^0E;ccWmNZ;?oE?? zjCFWx9!anEGq#0(Wx*N7cF?vH@K?rq=6JE$=z4YuSaK)yF zN}$5W?xl$*Vc3Us`YfEYzxUmQ8$PsiEDUlU>Rkv!G!NzTD)`}Q zK1v@xA`};7FgSm5ehOaAXZwV@8T1HGSV9xD79a|DpA_KjX`v2|ter2?KhDa zyWUsZu$&joae2`gmqHTNl{BKGYK@cJ7u2{MGEOnSfQ;iciy*3Hg`}=j_b`5DT^;Kr zUP7|e$SzXWqt8q8COb+v&QXHRUq~YA&7fC%4D>c&rjIVC38Q7=+Nd)ZUza|mvn<6&!L_iMT zmK>;J(Un3(d>H{!?3Obemlkw6@E`7y(?VosmWZ!pkTBT@QS@dhx{Odo*Q&J~kJCoj z92t``7f71o|V8f-TyY*YTpU=e7~E3k(iKNCc3HYla@EunU& zV8hD_yFu6`;oln!wP}P3df=3.10","schema_version":1,"sha256":"98dc5a49e77864fd529c647cc5e81181b9f5ebda234561135a80d4a05823f06d","source_hash":"c0fd650bfd08f8e8550cb88fb1d5b1649203bd44367dcd450f78c9928b8ce6dd"} +{"builder":"scripts/build_codex_plugin.py","python_requires":">=3.10","schema_version":1,"sha256":"7ea67b6a17d8f44c03b70e29ec29497a00edd0f9c4262e59cb9082f2015eafe6","source_hash":"ab9dfa18a1ddf72f65a64cc7aba86ca0bd1b4e79e0747c983078e5b8ee87e042"} diff --git a/plugins/marginal/scripts/marginal_control.py b/plugins/marginal/scripts/marginal_control.py new file mode 100644 index 0000000..813442d --- /dev/null +++ b/plugins/marginal/scripts/marginal_control.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Run MARGINAL's bundled control plane against Codex plugin data.""" + +from __future__ import annotations + +import os +import sys +from pathlib import Path + + +def _plugin_data() -> Path: + configured = os.environ.get("PLUGIN_DATA") + if configured: + return Path(configured).expanduser().resolve() + codex_home = os.environ.get("CODEX_HOME") + home = Path(codex_home).expanduser() if codex_home else Path.home() / ".codex" + return (home / "plugins" / "data" / "marginal-marginal").resolve() + + +def main(argv: list[str] | None = None) -> int: + arguments = list(sys.argv[1:] if argv is None else argv) + if not arguments or arguments[0] not in { + "status", + "doctor", + "review", + "promote", + "demote", + }: + print( + "usage: marginal_control.py {status|doctor|review|promote|demote} [options]", + file=sys.stderr, + ) + return 2 + + plugin_root = Path(__file__).resolve().parents[1] + runtime = plugin_root / "runtime" / "marginal_runtime.pyz" + if not runtime.is_file(): + print(f"MARGINAL runtime not found: {runtime}", file=sys.stderr) + return 1 + + data = _plugin_data() + data.mkdir(parents=True, exist_ok=True, mode=0o700) + environment = { + name: value + for name in ("PATH", "CODEX_HOME", "LANG", "LC_ALL", "SYSTEMROOT") + if (value := os.environ.get(name)) is not None + } + environment["PLUGIN_DATA"] = str(data) + environment["PLUGIN_ROOT"] = str(plugin_root) + os.execve( + sys.executable, + [ + sys.executable, + str(runtime), + "codex", + arguments[0], + "--data-dir", + str(data), + *arguments[1:], + ], + environment, + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/plugins/marginal/skills/marginal/SKILL.md b/plugins/marginal/skills/marginal/SKILL.md index eb9d121..239d996 100644 --- a/plugins/marginal/skills/marginal/SKILL.md +++ b/plugins/marginal/skills/marginal/SKILL.md @@ -9,15 +9,36 @@ Treat compute as scarce and claims as evidence-bound. The plugin starts globally it may exercise repository-scoped Tool Enforcement only after a valid Earned Enforcement receipt and explicit promotion. +## Native control + +Do not require a global `marginal` executable or a pip installation. Resolve the native plugin: + +1. Run `codex plugin list --json`. +2. Select the exact `pluginId` `marginal@marginal` and read its `source.path` as the plugin root. +3. On macOS/Linux, run `python3 /scripts/marginal_control.py COMMAND`; on Windows, + run `py -3 \scripts\marginal_control.py COMMAND`. + +Pass `--workspace ` and `--json` when inspecting repository-scoped state. The launcher +uses Codex's native plugin data directory, so hook evidence and control commands share one state. + ## Workflow -1. Run `marginal codex status` before describing the active mode. -2. Run `marginal codex doctor` when hooks, coverage, or compatibility are uncertain. -3. Use `/hooks` to inspect and grant trust to the exact lifecycle commands. -4. Run `marginal codex review`, then label each local redacted candidate with +1. Run the native `status` command before describing the active mode. +2. Read live operation, prior evidence, and enforcement as separate facts: + - `hooks_active: true` attests a live authenticated MARGINAL lifecycle service for this + repository. It is the strongest available operational signal, but does not expose or prove a + raw chat session identity. + - `hooks_observed: true` proves lifecycle evidence exists for this repository, even when no + session is currently live. + - `hooks_observed: false` means not yet observed, not that hooks are disabled. Use `/hooks` to + review and trust the exact definitions, perform a tool action, then run `status` again. + - `mode` reports `shadow` or repository-scoped `enforce`; installation alone never implies + enforcement. +3. Run `doctor` when hooks, coverage, or compatibility remain uncertain. +4. Run `review`, then label each local redacted candidate with `--candidate HASH --verdict waste|helpful` before promotion. -5. Run `marginal codex promote` only when the evidence receipt is ready. -6. Run `marginal codex demote` whenever identity, coverage, outcome observability, or policy drifts. +5. Run `promote` only when the evidence receipt is ready. +6. Run `demote` whenever identity, coverage, outcome observability, or policy drifts. ## Claims contract @@ -31,10 +52,10 @@ and explicit promotion. | Need | Command | | --- | --- | -| Current mode | `marginal codex status` | -| Capability diagnosis | `marginal codex doctor` | -| Unreviewed evidence | `marginal codex review` | -| Evidence-gated enforcement | `marginal codex promote` | -| Immediate fail-open reset | `marginal codex demote` | +| Current mode and hook evidence | `python3 /scripts/marginal_control.py status --workspace --json` | +| Capability diagnosis | `python3 /scripts/marginal_control.py doctor --json` | +| Unreviewed evidence | `python3 /scripts/marginal_control.py review --workspace --json` | +| Evidence-gated enforcement | `python3 /scripts/marginal_control.py promote --workspace --json` | +| Immediate fail-open reset | `python3 /scripts/marginal_control.py demote --workspace --json` | If evidence is incomplete or contradictory, keep Shadow Mode and report the exact blocking reason. diff --git a/pyproject.toml b/pyproject.toml index 471b731..5c07dcb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "marginal-ai" -version = "0.3.1" +version = "0.3.2" description = "Universal learning-loop and compute-governance foundation for economically disciplined AI agents." readme = "README.md" requires-python = ">=3.10" diff --git a/scripts/build_codex_plugin.py b/scripts/build_codex_plugin.py index 1844b8e..9185792 100644 --- a/scripts/build_codex_plugin.py +++ b/scripts/build_codex_plugin.py @@ -12,9 +12,17 @@ from pathlib import Path _ZIP_TIMESTAMP = (2020, 1, 1, 0, 0, 0) -_MAIN = ( - b"from marginal.integrations.codex.service import hook_main\nraise SystemExit(hook_main())\n" -) +_MAIN = b"""import sys + +if len(sys.argv) > 1 and sys.argv[1] != "--serve": + from marginal.cli import main + + raise SystemExit(main(sys.argv[1:])) + +from marginal.integrations.codex.service import hook_main + +raise SystemExit(hook_main()) +""" @dataclass(frozen=True, slots=True) diff --git a/scripts/smoke_codex_plugin.py b/scripts/smoke_codex_plugin.py index 94ce9e6..0e213d9 100644 --- a/scripts/smoke_codex_plugin.py +++ b/scripts/smoke_codex_plugin.py @@ -21,6 +21,8 @@ class CodexPluginSmokeResult: hook_coverage: float evidence_records: int completed_sessions: int + native_control_observed: bool + native_control_mode: str raw_secret_occurrences: int removed: bool codex_version: str @@ -116,7 +118,7 @@ def smoke_plugin( root = isolation_root.resolve() home = root / "home" codex_home = root / "codex" - plugin_data = root / "plugin-data" + plugin_data = codex_home / "plugins" / "data" / "marginal-marginal" workspace = root / "workspace" for directory in (home, codex_home, plugin_data): directory.mkdir(parents=True, exist_ok=True) @@ -153,6 +155,8 @@ def smoke_plugin( secret = "MARGINAL_SMOKE_SECRET_7fcd98" completed_hooks = 0 shadow_blocks = 0 + native_control_observed = False + native_control_mode = "unknown" removed = False try: for payload in _hook_payloads(workspace, secret): @@ -171,6 +175,21 @@ def smoke_plugin( sessions_root = plugin_data / "sessions" while list(sessions_root.glob("*.json")) and time.monotonic() < deadline: time.sleep(0.02) + control = _run( + [ + sys.executable, + str(plugin_root / "scripts" / "marginal_control.py"), + "status", + "--workspace", + str(workspace), + "--json", + ], + environment=environment, + cwd=workspace, + ) + control_status = json.loads(control.stdout) + native_control_observed = control_status.get("hooks_observed") is True + native_control_mode = str(control_status.get("mode", "unknown")) finally: remove = _run( [str(codex), "plugin", "remove", "marginal@marginal", "--json"], @@ -198,6 +217,8 @@ def smoke_plugin( if record.get("event") == "session_end" and record.get("session_hash") } ), + native_control_observed=native_control_observed, + native_control_mode=native_control_mode, raw_secret_occurrences=_count_secret(plugin_data, secret), removed=removed, codex_version=version, diff --git a/site/privacy.html b/site/privacy.html index f09fa5d..0b48df8 100644 --- a/site/privacy.html +++ b/site/privacy.html @@ -1,4 +1,3 @@ MARGINAL Privacy -

Effective 2026-08-13

MARGINAL Privacy

MARGINAL is local-first. The Codex plugin makes no network request and does not operate a SignalLayer Labs telemetry service.

Raw prompts, source, commands, tool output, transcripts, authentication files and credential environment values are not persisted by default. Local evidence is limited to hashes, structured decisions, coverage, outcomes, latency, review labels and receipts in user-private plugin data.

Uninstall preserves evidence; explicit marginal uninstall codex --purge-data --yes removes it. Third-party platforms retain their own privacy terms.

Complete privacy notice →

- +

Effective 2026-08-13

MARGINAL Privacy

MARGINAL is local-first. The Codex plugin makes no network request and does not operate a SignalLayer Labs telemetry service.

Raw prompts, source, commands, tool output, transcripts, authentication files and credential environment values are not persisted by default. Local evidence is limited to hashes, structured decisions, coverage, outcomes, latency, review labels and receipts in user-private plugin data.

codex plugin remove marginal@marginal preserves evidence; the optional Python package provides an explicit purge command. Third-party platforms retain their own privacy terms.

Complete privacy notice →

diff --git a/site/support.html b/site/support.html index 4872446..6ec2b30 100644 --- a/site/support.html +++ b/site/support.html @@ -1,4 +1,3 @@ MARGINAL Support -

Support

Get help without sharing secrets.

Use GitHub Issues for reproducible bugs and GitHub Discussions for design questions. Security and private privacy reports use GitHub private vulnerability reporting.

Include redacted marginal codex doctor output, versions and operating system. Never attach auth files, prompts, source, commands, tool output, transcripts, tokens or connection files.

Complete support policy →

- +

Support

Get help without sharing secrets.

Use GitHub Issues for reproducible bugs and GitHub Discussions for design questions. Security and private privacy reports use GitHub private vulnerability reporting.

Ask $marginal for redacted native doctor output, then include versions and operating system. Never attach auth files, prompts, source, commands, tool output, transcripts, tokens or connection files.

Complete support policy →

diff --git a/src/marginal/__init__.py b/src/marginal/__init__.py index 904dc46..8234636 100644 --- a/src/marginal/__init__.py +++ b/src/marginal/__init__.py @@ -137,4 +137,4 @@ "validate_safe_telemetry_record", ] -__version__ = "0.3.1" +__version__ = "0.3.2" diff --git a/src/marginal/integrations/codex/commands.py b/src/marginal/integrations/codex/commands.py index cb51685..671b730 100644 --- a/src/marginal/integrations/codex/commands.py +++ b/src/marginal/integrations/codex/commands.py @@ -19,6 +19,10 @@ write_promotion_receipt, ) from .service import read_mode +from .transport import ConnectionInfo, request_session + +_MAX_SESSION_RECEIPTS = 64 +_MAX_SESSION_RECEIPT_BYTES = 16_384 def default_data_dir() -> Path: @@ -68,6 +72,40 @@ def _emit(payload: dict[str, Any], *, as_json: bool) -> None: print(f"{key}: {value}") +def _live_session_repository(path: Path) -> str | None: + if path.is_symlink() or path.stat().st_size > _MAX_SESSION_RECEIPT_BYTES: + return None + connection = ConnectionInfo.from_file(path) + if connection.host != "127.0.0.1" or len(connection.token.encode("utf-8")) < 16: + return None + response = request_session(connection, operation="status", payload={}, timeout=0.25) + if response.get("ok") is not True: + return None + result = response.get("result") + repository_hash = result.get("repository_hash") if isinstance(result, dict) else None + return repository_hash if isinstance(repository_hash, str) else None + + +def _active_hook_sessions(data_dir: Path, *, repository_hash: str) -> tuple[int, int]: + sessions_root = data_dir / "sessions" + if not sessions_root.is_dir(): + return 0, 0 + active = 0 + stale = 0 + for index, path in enumerate(sessions_root.glob("*.json")): + if index >= _MAX_SESSION_RECEIPTS: + break + try: + session_repository = _live_session_repository(path) + except (OSError, ValueError, KeyError): + session_repository = None + if session_repository is None: + stale += 1 + elif session_repository == repository_hash: + active += 1 + return active, stale + + def codex_command( command: str, *, @@ -84,11 +122,38 @@ def codex_command( payload: dict[str, Any] if command == "status": state = read_mode(root, repository_hash=identity.repository_hash) + records = evidence_store.read_all() + summary = summarize_evidence(records) + hooks_observed = any( + record.get("event") in {"session_start", "decision", "outcome", "session_end"} + for record in records + ) + coverage_ratio = ( + summary.covered_actions / summary.coverable_actions + if summary.coverable_actions + else 0.0 + ) + active_hook_sessions, stale_session_receipts = _active_hook_sessions( + root, + repository_hash=identity.repository_hash, + ) + hooks_active = active_hook_sessions > 0 _emit( { **state, "capability": "Tool Enforcement", "repository_hash": identity.repository_hash, + "hook_state": ( + "active" if hooks_active else "observed" if hooks_observed else "not_observed" + ), + "hooks_observed": hooks_observed, + "hooks_active": hooks_active, + "active_hook_sessions": active_hook_sessions, + "stale_session_receipts": stale_session_receipts, + "evidence_records": len(records), + "covered_actions": summary.covered_actions, + "coverable_actions": summary.coverable_actions, + "coverage_ratio": coverage_ratio, }, as_json=as_json, ) diff --git a/src/marginal/integrations/codex/identity.py b/src/marginal/integrations/codex/identity.py index 1118c68..488f21b 100644 --- a/src/marginal/integrations/codex/identity.py +++ b/src/marginal/integrations/codex/identity.py @@ -10,7 +10,7 @@ from .installer import CommandRunner, SubprocessRunner, inspect_codex from .promotion import PromotionIdentity -PLUGIN_VERSION = "0.3.1" +PLUGIN_VERSION = "0.3.2" ADAPTER_VERSION = "1" POLICY_HASH = hashlib.sha256(b"marginal:no-progress:v1:max-same-evidence=2").hexdigest() DEFAULT_HOOK_HASH = "46b7a85a3a542957d055c615ab501f9fee284bb3193ac9ecfbe8951cce5a9942" diff --git a/src/marginal/integrations/codex/service.py b/src/marginal/integrations/codex/service.py index 5125a3b..42c798f 100644 --- a/src/marginal/integrations/codex/service.py +++ b/src/marginal/integrations/codex/service.py @@ -57,7 +57,10 @@ def _handler( ) -> Any: def handle(operation: str, payload: dict[str, Any]) -> dict[str, Any] | None: if operation == "status": - return runtime.summary() + return { + **runtime.summary(), + "repository_hash": identity.repository_hash, + } if operation == "close": runtime.close() evidence_store.append( diff --git a/tests/integrations/codex/test_commands.py b/tests/integrations/codex/test_commands.py index 176031f..b7496b9 100644 --- a/tests/integrations/codex/test_commands.py +++ b/tests/integrations/codex/test_commands.py @@ -4,10 +4,13 @@ import subprocess from pathlib import Path +import marginal.integrations.codex.commands as commands_module from marginal.integrations.codex.commands import codex_command +from marginal.integrations.codex.events import SessionEvent from marginal.integrations.codex.evidence import EvidenceStore, summarize_evidence from marginal.integrations.codex.identity import current_promotion_identity from marginal.integrations.codex.promotion import enforcement_is_active +from marginal.integrations.codex.service import start_session_service, stop_session_service def _git(repo: Path, *args: str) -> None: @@ -32,6 +35,125 @@ def test_status_defaults_to_shadow_without_receipt(tmp_path: Path, capsys) -> No assert exit_code == 0 assert payload["mode"] == "shadow" assert payload["capability"] == "Tool Enforcement" + assert payload["hook_state"] == "not_observed" + assert payload["hooks_observed"] is False + assert payload["hooks_active"] is False + assert payload["active_hook_sessions"] == 0 + assert payload["evidence_records"] == 0 + + +def test_status_reports_observed_hooks_from_repository_evidence(tmp_path: Path, capsys) -> None: + workspace = _repository(tmp_path / "repo") + data = tmp_path / "data" + identity = current_promotion_identity(workspace) + store = EvidenceStore(data / "evidence" / identity.repository_hash) + store.append( + { + "schema_version": 1, + "event": "session_start", + "session_hash": "session", + } + ) + store.append( + { + "schema_version": 1, + "event": "decision", + "session_hash": "session", + "action_hash": "action", + "semantic_key": "semantic", + "state_hash": "state", + "evidence_hash": "evidence", + "reason_code": "APPROVED", + "latency_ms": 2.0, + "covered": True, + "coverable": True, + "recommended_stop": False, + "reviewed": False, + "false_stop": False, + "pending": True, + } + ) + + exit_code = codex_command( + "status", + data_dir=data, + workspace=workspace, + as_json=True, + ) + + payload = json.loads(capsys.readouterr().out) + assert exit_code == 0 + assert payload["mode"] == "shadow" + assert payload["hook_state"] == "observed" + assert payload["hooks_observed"] is True + assert payload["evidence_records"] == 2 + assert payload["covered_actions"] == 1 + assert payload["coverable_actions"] == 1 + assert payload["coverage_ratio"] == 1.0 + + +def test_status_attests_live_hook_service_for_current_repository(tmp_path: Path, capsys) -> None: + workspace = _repository(tmp_path / "repo") + data = tmp_path / "data" + event = SessionEvent( + session_id="live-session", + cwd=str(workspace), + hook_event_name="SessionStart", + model="gpt-5.6-sol", + permission_mode="default", + source="startup", + ) + start_session_service(event, data_root=data) + try: + exit_code = codex_command( + "status", + data_dir=data, + workspace=workspace, + as_json=True, + ) + payload = json.loads(capsys.readouterr().out) + finally: + stop_session_service("live-session", data_root=data) + + assert exit_code == 0 + assert payload["hook_state"] == "active" + assert payload["hooks_active"] is True + assert payload["active_hook_sessions"] == 1 + + +def test_status_refuses_non_loopback_session_receipts( + tmp_path: Path, + capsys, + monkeypatch, +) -> None: + sessions = tmp_path / "sessions" + sessions.mkdir() + receipt = sessions / "untrusted.json" + receipt.write_text( + json.dumps( + { + "session_id": "untrusted", + "host": "198.51.100.1", + "port": 443, + "token": "untrusted-token-000000000000", + "pid": 1, + "connection_file": str(receipt), + } + ), + encoding="utf-8", + ) + + def unexpected_request(*args, **kwargs): + raise AssertionError("status must not contact a non-loopback endpoint") + + monkeypatch.setattr(commands_module, "request_session", unexpected_request) + + exit_code = codex_command("status", data_dir=tmp_path, as_json=True) + payload = json.loads(capsys.readouterr().out) + + assert exit_code == 0 + assert payload["hooks_active"] is False + assert payload["stale_session_receipts"] == 1 def test_unready_promotion_returns_two(tmp_path: Path, capsys) -> None: diff --git a/tests/integrations/codex/test_marketplace_smoke.py b/tests/integrations/codex/test_marketplace_smoke.py index 17a3d86..66524db 100644 --- a/tests/integrations/codex/test_marketplace_smoke.py +++ b/tests/integrations/codex/test_marketplace_smoke.py @@ -22,5 +22,7 @@ def test_marketplace_install_and_remove(tmp_path: Path) -> None: assert result.hook_coverage == 1.0 assert result.evidence_records >= 4 assert result.completed_sessions == 1 + assert result.native_control_observed is True + assert result.native_control_mode == "shadow" assert result.raw_secret_occurrences == 0 assert result.removed is True diff --git a/tests/plugin/test_codex_plugin.py b/tests/plugin/test_codex_plugin.py index e791a25..1c92fbb 100644 --- a/tests/plugin/test_codex_plugin.py +++ b/tests/plugin/test_codex_plugin.py @@ -2,6 +2,7 @@ import hashlib import json +import os import subprocess import sys import zipfile @@ -116,6 +117,134 @@ def test_generated_runtime_matches_provenance(tmp_path: Path) -> None: assert rebuilt.source_hash == provenance["source_hash"] +def test_generated_runtime_exposes_native_codex_control_plane(tmp_path: Path) -> None: + workspace = tmp_path / "repo" + workspace.mkdir() + subprocess.run(["git", "init", "-q"], cwd=workspace, check=True) + runtime = build_plugin_runtime(REPO, output_dir=tmp_path / "runtime").zipapp + + completed = subprocess.run( + [ + sys.executable, + str(runtime), + "codex", + "status", + "--data-dir", + str(tmp_path / "data"), + "--workspace", + str(workspace), + "--json", + ], + cwd=workspace, + check=False, + capture_output=True, + text=True, + timeout=10, + ) + + assert completed.returncode == 0, completed.stderr + payload = json.loads(completed.stdout) + assert payload["mode"] == "shadow" + assert payload["hook_state"] == "not_observed" + + +def test_native_control_launcher_uses_codex_plugin_data_without_python_install( + tmp_path: Path, +) -> None: + workspace = tmp_path / "repo" + workspace.mkdir() + subprocess.run(["git", "init", "-q"], cwd=workspace, check=True) + codex_home = tmp_path / "codex" + data = codex_home / "plugins" / "data" / "marginal-marginal" + repository_hash = hashlib.sha256(str(workspace.resolve()).encode()).hexdigest() + evidence = data / "evidence" / repository_hash / "evidence.jsonl" + evidence.parent.mkdir(parents=True) + evidence.write_text( + json.dumps( + { + "schema_version": 1, + "event": "session_start", + "session_hash": "session", + } + ) + + "\n", + encoding="utf-8", + ) + environment = { + "CODEX_HOME": str(codex_home), + "HOME": str(tmp_path / "home"), + "PATH": os.environ.get("PATH", ""), + } + + completed = subprocess.run( + [ + sys.executable, + str(PLUGIN / "scripts" / "marginal_control.py"), + "status", + "--workspace", + str(workspace), + "--json", + ], + cwd=workspace, + env=environment, + check=False, + capture_output=True, + text=True, + timeout=10, + ) + + assert completed.returncode == 0, completed.stderr + payload = json.loads(completed.stdout) + assert payload["hooks_observed"] is True + assert payload["evidence_records"] == 1 + + +def test_native_control_preserves_codex_home_for_public_cli_discovery(tmp_path: Path) -> None: + codex_home = tmp_path / "codex" + binary_dir = tmp_path / "bin" + binary_dir.mkdir() + fake_codex = binary_dir / "codex" + fake_codex.write_text( + """#!/bin/sh +if [ -z "$CODEX_HOME" ]; then + exit 9 +fi +if [ "$1" = "--version" ]; then + echo "codex-cli 0.147.0" +else + printf 'hooks stable true\nplugins stable true\n' +fi +""", + encoding="utf-8", + ) + fake_codex.chmod(0o755) + environment = { + "CODEX_HOME": str(codex_home), + "HOME": str(tmp_path / "home"), + "PATH": str(binary_dir), + } + + completed = subprocess.run( + [ + sys.executable, + str(PLUGIN / "scripts" / "marginal_control.py"), + "doctor", + "--json", + ], + env=environment, + check=False, + capture_output=True, + text=True, + timeout=10, + ) + + assert completed.returncode == 0, completed.stderr + payload = json.loads(completed.stdout) + assert payload["available"] is True + assert payload["hooks_enabled"] is True + assert payload["plugins_enabled"] is True + + def test_plugin_runtime_contains_no_live_repository_paths() -> None: runtime = (PLUGIN / "runtime" / "marginal_runtime.pyz").read_bytes() @@ -132,8 +261,11 @@ def test_skill_teaches_truthful_earned_enforcement_workflow() -> None: for phrase in ( "Shadow Mode", "Tool Enforcement", - "marginal codex status", - "marginal codex promote", + "codex plugin list --json", + "scripts/marginal_control.py", + "hooks_active", + "hooks_observed", "never claim token savings", ): assert phrase.casefold() in text.casefold() + assert "`marginal codex" not in text diff --git a/tests/test_public_api_v2.py b/tests/test_public_api_v2.py index 4166e25..0f052f8 100644 --- a/tests/test_public_api_v2.py +++ b/tests/test_public_api_v2.py @@ -23,7 +23,7 @@ def test_v02_public_exports_and_version() -> None: "ValueEstimate", } assert expected.issubset(set(marginal.__all__)) - assert marginal.__version__ == "0.3.1" + assert marginal.__version__ == "0.3.2" def test_json_schemas_exist_and_are_valid() -> None: diff --git a/tests/test_repository_consistency_v2.py b/tests/test_repository_consistency_v2.py index d1ea34b..840a7e2 100644 --- a/tests/test_repository_consistency_v2.py +++ b/tests/test_repository_consistency_v2.py @@ -20,7 +20,7 @@ def test_public_repository_identity_is_consistent() -> None: assert "SignalLayer Labs" in text or "SignalLayerLabs" in text, path codemeta = json.loads((ROOT / "codemeta.json").read_text(encoding="utf-8")) - assert codemeta["version"] == "0.3.1" + assert codemeta["version"] == "0.3.2" assert codemeta["codeRepository"] == "https://github.com/SignalLayerLabs/Marginal" assert codemeta["issueTracker"] == "https://github.com/SignalLayerLabs/Marginal/issues" assert codemeta["author"]["name"] == "SignalLayer Labs"