diff --git a/.github/assets/logo/logo-dark.png b/.github/assets/logo/logo-dark.png new file mode 100644 index 0000000..8832362 Binary files /dev/null and b/.github/assets/logo/logo-dark.png differ diff --git a/.github/assets/logo/logo-light.png b/.github/assets/logo/logo-light.png new file mode 100644 index 0000000..8832362 Binary files /dev/null and b/.github/assets/logo/logo-light.png differ diff --git a/.github/assets/social-preview/social-preview.png b/.github/assets/social-preview/social-preview.png new file mode 100644 index 0000000..03acd9e Binary files /dev/null and b/.github/assets/social-preview/social-preview.png differ diff --git a/.github/brand/brand.lock.json b/.github/brand/brand.lock.json deleted file mode 100644 index 1ce4134..0000000 --- a/.github/brand/brand.lock.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "schema_version": 2, - "authority": "https://github.com/computer-mcp/.github", - "revision": "2d9913a442afcb22faca90f5272d3988eb961be6e858b71c5067303cf7c627d6", - "consumer": "plugin-codex", - "files": { - "Documentation/Brand/header-light.png": "8ff8b0fa648a3e6c9b8e89626a14de9fcfc49eaa8fbe562253329f1d16228674", - "Documentation/Brand/header-dark.png": "96ecb466a74b98e236197e3601b5c37118b4cdb3e5cb1d2322c2e45bb2fce7d0", - ".github/brand/social.png": "3d3ce9c8a276fb698dd9fe5f876bdb83f8badf7da2a1e1129b5dc4711a9c96c1" - }, - "entries": { - "README.md": [ - "Codex", - "Documentation/Brand/header-light.png", - "Documentation/Brand/header-dark.png" - ] - } -} diff --git a/.github/brand/social.png b/.github/brand/social.png deleted file mode 100644 index ae58cc5..0000000 Binary files a/.github/brand/social.png and /dev/null differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24d1e38..412a79c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,14 +1,15 @@ name: CI on: pull_request: + types: [opened, synchronize, reopened] push: branches: [master] workflow_dispatch: permissions: contents: read concurrency: - group: ci-${{ github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: package: runs-on: macos-latest @@ -27,16 +28,16 @@ jobs: env: BASE_COMMIT: ${{ github.event.pull_request.base.sha || github.event.before }} run: | - python3 Scripts/version.py check + python3 scripts/version.py check if [[ "$BASE_COMMIT" =~ ^[0-9a-f]{40}$ && "$BASE_COMMIT" != 0000000000000000000000000000000000000000 ]]; then - python3 Scripts/version.py check --base "$BASE_COMMIT" + python3 scripts/version.py check --base "$BASE_COMMIT" fi /usr/bin/swift --version /usr/bin/swift package resolve git diff --exit-code -- Package.resolved - Scripts/verify-swift-codex-release-gate.sh + scripts/verify-swift-codex-release-gate.sh node --test Tests/schema-import.test.mjs - node Scripts/import-schema.mjs --check + node scripts/import-schema.mjs --check /usr/bin/swift format lint --strict --recursive Package.swift Sources Tests /usr/bin/swift build --build-tests --disable-automatic-resolution - name: Run Swift tests @@ -63,11 +64,11 @@ jobs: - name: Validate package ownership and publication run: | python3 -m unittest discover -s Tests -p 'test_*.py' -v - python3 Scripts/check-workflow.py --check-release-workflow + python3 scripts/check-workflow.py --check-release-workflow - name: Build relocatable adapter and receipt run: | - python3 Scripts/package.py --output dist --configuration release - python3 Scripts/check-package.py --archive dist/codex-plugin-macos-arm64.zip --receipt dist/receipt.json --destination .build/accepted-package + python3 scripts/package.py --output dist --configuration release + python3 scripts/check-package.py --archive dist/codex-plugin-macos-arm64.zip --receipt dist/receipt.json --destination .build/accepted-package - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: codex-plugin-macos-arm64-${{ github.sha }} @@ -77,5 +78,9 @@ jobs: if-no-files-found: error retention-days: 14 - brand: - uses: computer-mcp/.github/.github/workflows/brand-check.yml@f402911abcdfb87ef0c41a2759e8c51387b18935 # master + validate: + if: always() + needs: [package] + uses: computer-mcp/github-workflows/.github/workflows/result.yml@d65bc20504f39c3a30232d67043abeca43d2c7b1 + with: + needs-json: ${{ toJSON(needs) }} diff --git a/.github/workflows/notify-catalog.yml b/.github/workflows/notify-catalog.yml index 4b3a1e6..f067937 100644 --- a/.github/workflows/notify-catalog.yml +++ b/.github/workflows/notify-catalog.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Request complete catalog reconciliation id: catalog - uses: computer-mcp/computer-mcp.github.io/.github/actions/notify-catalog@cda692839487f03a2b4740d945f9a8bd818554e5 + uses: computer-mcp/github-workflows/actions/notify-catalog@d65bc20504f39c3a30232d67043abeca43d2c7b1 with: client-id: ${{ vars.CATALOG_APP_CLIENT_ID }} private-key: ${{ secrets.CATALOG_APP_PRIVATE_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7987cb..366e89e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,9 +15,6 @@ on: type: boolean default: false permissions: {} -concurrency: - group: plugin-release-${{ github.repository }}-${{ inputs.release_tag || inputs.source_run }} - cancel-in-progress: false jobs: verify: if: github.repository == 'computer-mcp/plugin-codex' && github.ref == 'refs/heads/master' @@ -52,7 +49,7 @@ jobs: shell: bash run: | set -euo pipefail - python3 -I Scripts/plugin_release.py verify \ + python3 -I scripts/plugin_release.py verify \ --repository "$GH_REPOSITORY" --source-run "$SOURCE_RUN" \ --tag "$RELEASE_TAG" --publish "$RELEASE_PUBLISH" \ --workflow-ref "$RELEASE_WORKFLOW_REF" --workflow-sha "$RELEASE_WORKFLOW_SHA" \ @@ -60,7 +57,7 @@ jobs: python3 -I - <<'PY' import hashlib, os from pathlib import Path - data = Path('Scripts/plugin_release.py').read_bytes() + data = Path('scripts/plugin_release.py').read_bytes() Path('.build/plugin-release/plugin_release.py').write_bytes(data) with open(os.environ['GITHUB_OUTPUT'], 'a') as output: output.write('helper_digest=' + hashlib.sha256(data).hexdigest() + '\n') diff --git a/.github/workflows/source-policy.yml b/.github/workflows/source-policy.yml new file mode 100644 index 0000000..6179997 --- /dev/null +++ b/.github/workflows/source-policy.yml @@ -0,0 +1,17 @@ +name: Source policy checks +on: + pull_request: + types: [opened, synchronize, reopened, edited] + push: + branches: [master] + merge_group: +permissions: + contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +jobs: + policy: + uses: computer-mcp/github-workflows/.github/workflows/policy.yml@d65bc20504f39c3a30232d67043abeca43d2c7b1 + with: + declaration-revision: 5c54a40cb7953db7472ebbefd3db608036b7b3ab diff --git a/AGENTS.md b/AGENTS.md index 9c2451d..6490c60 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -162,16 +162,6 @@ Do not install or bundle vendor Codex binaries. Keep generated schema provenance Use Swift Testing. Run strict swift-format lint, swift build, swift test, schema drift checks, and an isolated standard MCP client before handoff. Use `Documentation/Reference/Release.md` for packaged runtime acceptance through -the installed host; run `Scripts/check-host.py --help` and its Python unit tests +the installed host; run `scripts/check-host.py --help` and its Python unit tests when changing that driver. Logs must never write to the protocol stdout. Do not publish repositories or releases implicitly. - -## Brand delivery - -The organization `.github` repository owns the design system and renders every -image; the main Computer MCP repository owns ProductIdentity. Imported -`Documentation/Brand/header-light.png` and `header-dark.png` travel with the -packaged manual; `.github/brand/social.png` supplies the repository preview. -`.github/brand/brand.lock.json` binds them to the organization exports, and CI -verifies it with the shared brand-check workflow. Update imports with the -organization repository's `python3 Brand/brand.py sync`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 05c10b4..66bb202 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,19 +10,19 @@ CI runs these checks on macOS; run them before opening a pull request. They need Node.js, `jq` and `ripgrep`: ```sh -python3 Scripts/version.py check +python3 scripts/version.py check swift package resolve && git diff --exit-code -- Package.resolved -Scripts/verify-swift-codex-release-gate.sh +scripts/verify-swift-codex-release-gate.sh node --test Tests/schema-import.test.mjs -node Scripts/import-schema.mjs --check +node scripts/import-schema.mjs --check swift format lint --strict --recursive Package.swift Sources Tests swift build --build-tests --disable-automatic-resolution swift test --skip-build --disable-automatic-resolution --no-parallel python3 -m unittest discover -s Tests -p 'test_*.py' -python3 Scripts/package.py --output OUTPUT_DIRECTORY --configuration release +python3 scripts/package.py --output OUTPUT_DIRECTORY --configuration release ``` -CI also runs the organization brand check. Exercise help, valid input and invalid input +Exercise help, valid input and invalid input from outside the checkout. Keep command output contracts and generated resources covered by tests. diff --git a/Documentation/Architecture/Package.md b/Documentation/Architecture/Package.md index 674fadb..7d4ca81 100644 --- a/Documentation/Architecture/Package.md +++ b/Documentation/Architecture/Package.md @@ -332,7 +332,7 @@ and API documentation scope. The repository validation workflow resolves and verifies the checked-in dependency lock, lints and tests the Swift package, verifies package input ownership and -exclusive output publication, then invokes `Scripts/package.py` for a +exclusive output publication, then invokes `scripts/package.py` for a relocatable release-configuration archive. It retains the ZIP and file/digest receipt with read-only repository permissions. The version-controlled manifest owns archive compatibility; packaging verifies the built adapter's slices against diff --git a/Documentation/Brand/header-dark.png b/Documentation/Brand/header-dark.png deleted file mode 100644 index b58f370..0000000 Binary files a/Documentation/Brand/header-dark.png and /dev/null differ diff --git a/Documentation/Brand/header-light.png b/Documentation/Brand/header-light.png deleted file mode 100644 index 5fe5dfc..0000000 Binary files a/Documentation/Brand/header-light.png and /dev/null differ diff --git a/Documentation/Reference/Installation.md b/Documentation/Reference/Installation.md index 1bb4143..8ae15a6 100644 --- a/Documentation/Reference/Installation.md +++ b/Documentation/Reference/Installation.md @@ -12,7 +12,7 @@ From this independent repository, with Python 3.11 or newer and its Swift dependencies already resolved: ```sh -python3 Scripts/package.py --output /absolute/new/artifact-directory +python3 scripts/package.py --output /absolute/new/artifact-directory ``` The default build is release; `--configuration debug` is available for development. @@ -32,6 +32,15 @@ Computer MCP version. Official GitHub installation verifies the archive manifest against its release tag. See [third-party components](../../THIRD_PARTY_NOTICES.md) for distribution terms. +The archive includes `README.md`, `CONTRIBUTING.md`, `LICENSE`, +`THIRD_PARTY_NOTICES.md`, `Package.resolved`, `Documentation/`, the manifest, +the executable and resource bundle, and collected `ThirdPartyNotices/` and +`ThirdPartyNotices.txt`. README images retain their relative paths at +`.github/assets/logo/logo-light.png` and `.github/assets/logo/logo-dark.png`. +ZIP entries use sorted paths, fixed timestamps and normalized permissions; +identical staged file bytes and executable bits produce identical archive bytes. +Native rebuilds may still produce different executable bytes. + Package inputs must be regular files and directories. Symbolic links and special files are rejected before signing or running the staged adapter. The output is published atomically without replacing any existing destination, including an @@ -41,7 +50,7 @@ packager's temporary staging directory; existing outputs remain unchanged. The repository's CI workflow (`ci.yml`) runs on pull requests, `master` pushes and manual dispatch. It checks formatting, tests and the dependency lock, then retains the macOS ZIP and receipt as a downloadable workflow artifact. CI verifies -and relocates the exact archive with `Scripts/check-package.py`. These outputs +and relocates the exact archive with `scripts/check-package.py`. These outputs are validation builds, not published or verified official releases. Check the receipt's architecture before installation. Public distribution needs separate publisher authorization and its signing/provenance review. diff --git a/Documentation/Reference/Release.md b/Documentation/Reference/Release.md index e102bd3..ad1c1b1 100644 --- a/Documentation/Reference/Release.md +++ b/Documentation/Reference/Release.md @@ -53,7 +53,7 @@ requirements are: The [package dependency overview](../Architecture/Package.md#dependencies) records their architectural responsibilities. CI resolves the committed lock -and rejects drift. `Scripts/verify-swift-codex-release-gate.sh` checks the +and rejects drift. `scripts/verify-swift-codex-release-gate.sh` checks the declared exact swift-codex version and resolved revision against its public Git tag, accepting the tag's peeled commit when it is annotated. The SDK's public tag owns its release identity. @@ -67,12 +67,12 @@ combination's runtime acceptance. ## Derived Metadata and Drift Checks -`python3 Scripts/version.py update --kind --reason ` +`python3 scripts/version.py update --kind --reason ` updates the manifest and generated constant. After an explicit manifest edit, -`python3 Scripts/version.py generate` regenerates +`python3 scripts/version.py generate` regenerates `Sources/CodexAdapter/CodexAdapterBuildInfo.swift`. -`python3 Scripts/version.py check` is read-only and rejects generated metadata +`python3 scripts/version.py check` is read-only and rejects generated metadata drift. Its `--binary ` option checks the actual executable's `--version`. `check --base ` rejects version regression when HEAD has parents and the base is an available ancestor; otherwise it checks the current version and @@ -80,7 +80,7 @@ reports why comparison was skipped. CI supplies the pull request base or previous `master` commit. `check --tag ` verifies the manifest version and the tag's commit. -`node Scripts/import-schema.mjs --check` verifies bundled protocol resources +`node scripts/import-schema.mjs --check` verifies bundled protocol resources against the locked SDK commit; [Protocol inputs](../../README.md#protocol-inputs) owns regeneration instructions. Packaging checks the manifest, generated version and dependency lock, validates the built executable's version and @@ -89,8 +89,8 @@ checks the relocated bytes before runtime acceptance. ## Candidate, Acceptance and Publication -CI runs source checks and `Scripts/package.py`, then verifies and relocates the -archive with `Scripts/check-package.py`. It uploads the ZIP and native build +CI runs source checks and `scripts/package.py`, then verifies and relocates the +archive with `scripts/check-package.py`. It uploads the ZIP and native build receipt as the commit-named candidate artifact. `release.yml` promotes those accepted CI bytes and receipts without rebuilding. Verification, publication and catalog notification are separate jobs. @@ -105,12 +105,12 @@ not model execution. Run the ordinary checks in [Contributing](../../CONTRIBUTIN first. ```sh -python3 Scripts/check-package.py --archive --receipt \ +python3 scripts/check-package.py --archive --receipt \ --destination .build/accepted-package -python3 Scripts/check-workflow.py \ +python3 scripts/check-workflow.py \ --adapter "$PWD/.build/accepted-package/bin/codex-mcp-adapter" \ --codex /absolute/path/to/codex -python3 Scripts/check-host.py \ +python3 scripts/check-host.py \ --host /absolute/path/to/computer-mcp \ --adapter "$PWD/.build/accepted-package/bin/codex-mcp-adapter" \ --codex /absolute/path/to/codex \ @@ -217,7 +217,7 @@ python3 -m unittest discover -s Tests -p 'test_host_acceptance.py' -v The Release assets are exactly the manifest's `compatibility.artifacts` ZIPs, their `.receipt.json` native build receipts, `release-receipt.json` and `SHA256SUMS`. The native receipts remain inputs to `check-package.py`. -`Scripts/plugin_release.py` assembles the schema 1 receipt and checksums under +`scripts/plugin_release.py` assembles the schema 1 receipt and checksums under the organization's [release content contract](https://github.com/computer-mcp/.github/blob/master/VERSIONING.md#plugin-release-contents) and [Codex promotion contract](https://github.com/computer-mcp/.github/blob/master/VERSIONING.md#codex-promotion-exception). @@ -315,8 +315,8 @@ operator decision. | --- | --- | --- | | Version update and drift checks | [Metadata entry points](#derived-metadata-and-drift-checks) | Local source write for updates; read-only checks | | Source validation | [Contributing checks](../../CONTRIBUTING.md#validation) | Local toolchain; dependency resolution and public SDK tag lookup | -| Release workflow validation | `python3 Scripts/check-workflow.py --check-release-workflow` and `python3 -m unittest discover -s Tests -p 'test_*.py'` | Local files; no host, vendor or credentials | -| Candidate build | `python3 Scripts/package.py --output dist --configuration release` | Local build tools and resolved dependencies; new output directory | +| Release workflow validation | `python3 scripts/check-workflow.py --check-release-workflow` and `python3 -m unittest discover -s Tests -p 'test_*.py'` | Local files; no host, vendor or credentials | +| Candidate build | `python3 scripts/package.py --output dist --configuration release` | Local build tools and resolved dependencies; new output directory | | Archive and runtime acceptance | [Runtime acceptance](#runtime-acceptance) | Verified archive, installed host and vendor; authorized file-store login for full mode | | Dry run and publication | [Publish](#publish) | Actions dispatch; verified immutable-release setting; publication job has contents write access | | Interrupted-run recovery and catalog status | [Retry](#retry) | Actions access; catalog authority isolated to the notifier | diff --git a/README.md b/README.md index e7ba864..18be425 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,85 @@ - - - Computer MCP — Codex - - -# Computer MCP — Codex - -Part of the [Computer MCP](https://computer-mcp.github.io/) family. -**Wherever you chat, your computer is there.** - -This independent Swift package projects Codex execution through standard MCP. -It builds on swift-codex's App Server and Exec clients and the Computer MCP fork -of the MCP Swift SDK. -The host owns registration, caller grants, workspace authorization and audit. -The plugin does not link Computer MCP Core or install the vendor Codex binary. - -## Current capabilities - -The configured server exposes seven `codex.exec.*` tools for sessions, bounded -events, results, cancellation and retained-result release, alongside the App Server tools below. - -`codex.protocol.methods.list` and `codex.protocol.methods.describe` inspect bundled, -version-specific protocol declarations. Schema presence does not prove -execution support or authorization. - -App Server exposes thread/turn and Goal operations, approvals, user input, -events, runtime ownership and release, recent-thread inspection, acceptance -runs, worktree leases and operational diagnostics. Managed-worktree planning -and receipts use the adapter's database; provisioning and removal require a -connected host workspace service. - -Every SDK-adopted stable request also has a `codex.app.native.*` tool with its -complete native parameter schema. For example, `codex.app.native.fs.readFile` -takes `{"params":{"path":"/absolute/path"}}`. `codex.app.methods.list` and -`describe` report stability and operation risk; `codex.app.methods.call` shares -the native runtime validation and accepts experimental methods with -`experimental: true` when the runtime enables experimental API support. -Native tools preserve request extensions, response fields and exact signed -64-bit integers. Powerful operations still require the host's corresponding -authorization. Higher-level thread, approval and worktree workflows remain -available alongside the native tools. - -Thread reads return metadata by default. Use `codex.app.thread.turns.list` -and `codex.app.thread.items.list` for bounded history pages with native cursors. -Explicit full-history reads remain available through `include_turns: true`; -large histories can exceed transport, output or timeout limits. See the -[long-thread workflow](Documentation/Reference/Workflows.md#reading-long-threads). - -App Server, Exec, native Goals, acceptance runs, thread handoff and managed -worktrees are experimental and opt-in. Execution depends on a separately -installed, authenticated Codex version that supports the requested methods. -A completed turn does not imply Goal completion or acceptance. The adapter -cannot inspect or terminate another client's connection or processes. -Native Full Access uses the executing user's OS authority; host callbacks -still require their own grants. See [diagnosis and recovery](Documentation/Reference/HostIntegration.md#diagnostics) -and [release acceptance](Documentation/Reference/Release.md#runtime-acceptance). - -## Build and run - -Building requires macOS 14 or newer and Swift 6.2 or newer. -Run `swift build` and `swift test`. Launch -`.build/debug/codex-mcp-adapter` through an MCP stdio client, never as an -unbounded unattended shell command. `--help` prints usage without serving. - -Without arguments the server exposes only the two protocol inspection tools. -To enable the execution providers, save the existing Codex configuration fields -as a local JSON file: +

+ + + + +

+ +

Codex

+ +

Computer MCP family plugin for optional Codex execution and protocol inspection.

+ +

+ CI + Swift 6.2 or newer + Platform: macOS 14 or newer + License: FSL-1.1-ALv2 +

+ +

+ Install · + Usage · + Workflows · + Documentation · + Contributing +

+ +## Overview + +This independent Swift package serves Codex execution to [Computer MCP](https://computer-mcp.github.io/) +over standard MCP. It builds on swift-codex's App Server and Exec clients and the Computer MCP +fork of the MCP Swift SDK. The host owns registration, caller grants, project authorization and +audit; the plugin does not link Computer MCP Core or install the vendor Codex binary. + +Computer MCP supplies the initial directory and a stable authorization subject, and admits each +invocation using its current permissions. Codex controls its own sandbox and approvals: explicit +native directories and Full Access selections are preserved, and native Full Access uses the +executing user's operating-system authority. The initial directory is task ownership metadata, +not an operating-system sandbox. Host callbacks still require their own grants. + +## Quick look + +The server's tools. Without a configuration, only the two protocol inspection tools are +available; the others appear when you enable execution. + +| Tools | What they do | +| --- | --- | +| `codex.exec.*` | Seven tools for sessions, bounded events, results, cancellation and retained-result release | +| `codex.protocol.methods.list`, `.describe` | Inspect bundled, version-specific protocol declarations | +| App Server thread, turn and Goal tools | Approvals, user input, events, runtime ownership and release, recent threads, acceptance runs, worktree leases and diagnostics | +| `codex.app.native.*` | One tool per SDK-adopted stable request, with its complete native parameter schema | +| `codex.app.methods.list`, `describe`, `call` | Report stability and operation risk; call methods with the shared native validation | + +For example, `codex.app.native.fs.readFile` takes `{"params":{"path":"/absolute/path"}}`. + +## Install + +1. Build an archive with Python 3.11 or newer and the Swift dependencies resolved: + + ```sh + python3 scripts/package.py --output /absolute/new/artifact-directory + ``` + +2. Install it with the App's plugin install action, or with the owner-only CLI: + + ```sh + computer-mcp plugins install /absolute/path/ARCHIVE.zip --id codex --version VERSION --sha256 DIGEST --expected-revision REVISION + ``` + +The host launches the plugin's MCP server, `bin/codex-mcp-adapter`, over stdio. Installing never +installs, updates or removes Codex, changes `PATH` or grants a profile access to tools; execution +needs a separately installed, authenticated Codex that supports the requested methods. See +[installation](Documentation/Reference/Installation.md) for the receipt, architectures and +recovery. + +## Usage + +### Configuration + +Launch `codex-mcp-adapter` only through an MCP stdio client, never as an unbounded unattended +shell command. `--help` prints usage without serving. To enable execution, save the Codex +configuration fields as a local JSON file and launch with +`codex-mcp-adapter --config /absolute/path/codex.json`: ```json { @@ -75,60 +90,89 @@ as a local JSON file: } ``` -Launch with `codex-mcp-adapter --config /absolute/path/codex.json`. -For App Server execution, set `app_server_enabled` to `true` and supply -`--state-directory /absolute/path/adapter-state`. This directory holds the -adapter's `codex.sqlite` records; it is separate from the vendor's `CODEX_HOME` -and the Computer MCP host database. Host-bound Exec also requires this directory -for shared native thread ownership. Keep it across restarts and updates. -The configuration file is bounded to 64 KiB. No executable is started for -catalog discovery. Omitted sandbox and approval settings follow native Codex -configuration, including a user-selected Full Access default. Optional adapter -settings and explicit tool parameters are native overrides, not host grants. - -Computer MCP supplies the initial directory and stable authorization subject. -The host admits each invocation using its current permissions; Codex controls -its own sandbox and approvals. Explicit native directories and Full Access -selections are preserved. The initial directory is task ownership metadata, -not an operating-system sandbox. Standalone clients use their process working -directory as the initial directory. -`CODEX_HOME` remains the vendor's environment setting; do not put credentials -in arguments. Host metadata and parent session identity are not propagated to -the vendor process. Neither launch mode grants host administration. - -A host that explicitly enables `hostServices` supplies the scoped MCP callback -connection for dynamic tools, managed-workspace -registration and diagnostics. See [host integration](Documentation/Reference/HostIntegration.md) -for approval ownership, recovery and availability boundaries. - -See [workflows](Documentation/Reference/Workflows.md), -[package architecture](Documentation/Architecture/Package.md), and -[installation](Documentation/Reference/Installation.md). -The [documentation index](Documentation/README.md) and -[contributor guide](CONTRIBUTING.md) cover the repository's supporting material. - -## Protocol inputs - -The bundled inventory and adoption metadata derive from the exact swift-codex -commit in `Package.resolved`. swift-codex owns the upstream schema lock, -generation and adoption decisions. Regenerate the downstream resources after -resolving dependencies: +For App Server execution, set `app_server_enabled` to `true` and pass +`--state-directory /absolute/path/adapter-state`. The directory holds the adapter's `codex.sqlite` +records, separate from the vendor's `CODEX_HOME` and the Computer MCP host database; host-bound +Exec also uses it for shared native thread ownership. Keep it across restarts and updates. + +The configuration file is limited to 64 KiB, and no executable starts for catalog discovery. +Omitted sandbox and approval settings follow native Codex configuration, including a +user-selected Full Access default. Optional adapter settings and explicit tool parameters are +native overrides, not host grants. `CODEX_HOME` stays the vendor's environment setting; do not +put credentials in arguments. Host metadata and parent session identity are not passed to the +vendor process, and neither launch mode grants host administration. Standalone clients use their +process working directory as the initial directory. + +A host that enables `hostServices` supplies the scoped MCP callback connection for dynamic tools, +managed-workspace registration and diagnostics. See +[host integration](Documentation/Reference/HostIntegration.md) for approval ownership, recovery +and availability boundaries. + +### Capabilities and limits + +App Server, Exec, native Goals, acceptance runs, thread handoff and managed worktrees are +experimental and opt-in. A completed turn does not imply Goal completion or acceptance. The +adapter cannot inspect or terminate another client's connection or processes. Managed-worktree +planning and receipts use the adapter's database; provisioning and removal require a connected +host workspace service. + +Native tools preserve request extensions, response fields and exact signed 64-bit integers. +`codex.app.methods.call` accepts experimental methods with `experimental: true` when the runtime +enables experimental API support. Powerful operations still require the host's corresponding +authorization. Schema presence does not prove execution support or authorization; unsupported +vendor methods fail explicitly. + +Thread reads return metadata by default. Use `codex.app.thread.turns.list` and +`codex.app.thread.items.list` for bounded history pages with native cursors. Full-history reads +remain available through `include_turns: true`, but large histories can exceed transport, output +or timeout limits; see [reading long threads](Documentation/Reference/Workflows.md#reading-long-threads). +For failures, see [diagnosis and recovery](Documentation/Reference/HostIntegration.md#diagnostics). + +### Protocol inputs + +The bundled inventory and adoption metadata derive from the exact swift-codex commit in +`Package.resolved`; swift-codex owns the upstream schema lock, generation and adoption decisions. +Regenerate the downstream resources after resolving dependencies: ```sh -node Scripts/import-schema.mjs -node Scripts/import-schema.mjs --check +node scripts/import-schema.mjs +node scripts/import-schema.mjs --check node --test Tests/schema-import.test.mjs ``` -An optional SDK repository path supplies Git objects for the locked commit; -uncommitted files in that repository are never imported. The receipt binds the -SDK revision, upstream identity and derived resource digests. Do not edit the -resources by hand. Resource integrity is not publisher signature verification. +An optional SDK repository path supplies Git objects for the locked commit; uncommitted files in +that repository are never imported. The receipt binds the SDK revision, upstream identity and +derived resource digests. Do not edit the resources by hand. Resource integrity is not publisher +signature verification. + +`codex-mcp-adapter compare-schema BASELINE_JSON_DIRECTORY CURRENT_JSON_DIRECTORY` reports all four +message directions, source digests, method and schema changes, JSON Pointers and +required-parameter differences, without running Codex. The adapter and swift-codex share the same +vendor schema baseline; the installed Codex can differ, and declaration coverage does not +establish runtime support. + +## Requirements + +- Computer MCP on macOS 14 or newer. +- A separately installed, authenticated Codex for execution. +- Swift 6.2 or newer to build and test (`swift build`, `swift test`), and Python 3.11 or newer + to package. +- Node.js to regenerate the protocol resources. + +## Documentation + +The [documentation index](Documentation/README.md) links the +[workflows](Documentation/Reference/Workflows.md), [host integration](Documentation/Reference/HostIntegration.md), +[installation](Documentation/Reference/Installation.md), +[release acceptance](Documentation/Reference/Release.md#runtime-acceptance) and the +[package architecture](Documentation/Architecture/Package.md). + +## Contributing + +See the [contributor guide](CONTRIBUTING.md). -`codex-mcp-adapter compare-schema BASELINE_JSON_DIRECTORY CURRENT_JSON_DIRECTORY` -reports all four message directions, source digests, method and schema changes, -JSON Pointers, and required-parameter differences. It does not run Codex. +## License -The adapter and swift-codex use the same vendor schema baseline. The installed -Codex executable can differ; declaration coverage does not establish runtime -support. Unsupported vendor methods fail explicitly. +Computer MCP-owned code and resources use the +[Functional Source License 1.1, Apache 2.0 Future License](LICENSE) (FSL-1.1-ALv2). +[Third-party notices](THIRD_PARTY_NOTICES.md) list the components and their own licenses. diff --git a/Sources/CodexAdapter/CodexAdapterBuildInfo.swift b/Sources/CodexAdapter/CodexAdapterBuildInfo.swift index e7d7ccc..a8e4b1a 100644 --- a/Sources/CodexAdapter/CodexAdapterBuildInfo.swift +++ b/Sources/CodexAdapter/CodexAdapterBuildInfo.swift @@ -1,4 +1,4 @@ -// Generated by Scripts/version.py from computer-mcp-plugin.toml. +// Generated by scripts/version.py from computer-mcp-plugin.toml. package enum CodexAdapterBuildInfo { package static let version = "0.1.0" } diff --git a/Tests/Fixtures/NativeCodex/README.md b/Tests/Fixtures/NativeCodex/README.md index 02a73ad..897d981 100644 --- a/Tests/Fixtures/NativeCodex/README.md +++ b/Tests/Fixtures/NativeCodex/README.md @@ -1,7 +1,7 @@ # Native Codex model fixture `ModelServer.py` serves a private loopback Responses endpoint and network token. -It never executes commands. `Scripts/check-host.py` owns its input recipes, +It never executes commands. `scripts/check-host.py` owns its input recipes, workspaces, network token, process and native Codex generations. Each recipe issues at most one native `exec_command` call using a non-login shell. Exact `function_call_output` receipts are correlated to the issued call ID and checked diff --git a/Tests/schema-import.test.mjs b/Tests/schema-import.test.mjs index c8b0f8d..94b2886 100644 --- a/Tests/schema-import.test.mjs +++ b/Tests/schema-import.test.mjs @@ -4,7 +4,7 @@ import { execFileSync } from 'node:child_process'; import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { readLockedSnapshot, syncArtifacts } from '../Scripts/import-schema.mjs'; +import { readLockedSnapshot, syncArtifacts } from '../scripts/import-schema.mjs'; function fixture(t) { const root = mkdtempSync(join(tmpdir(), 'codex-schema-authority-')); diff --git a/Tests/test_host_acceptance.py b/Tests/test_host_acceptance.py index daacae4..1d64b1b 100644 --- a/Tests/test_host_acceptance.py +++ b/Tests/test_host_acceptance.py @@ -9,7 +9,7 @@ import unittest from unittest.mock import Mock, patch -SCRIPT = Path(__file__).resolve().parents[1] / "Scripts/check-host.py" +SCRIPT = Path(__file__).resolve().parents[1] / "scripts/check-host.py" spec = importlib.util.spec_from_file_location("host_acceptance", SCRIPT) acceptance = importlib.util.module_from_spec(spec) spec.loader.exec_module(acceptance) diff --git a/Tests/test_package.py b/Tests/test_package.py index 7c7318e..440664b 100644 --- a/Tests/test_package.py +++ b/Tests/test_package.py @@ -8,10 +8,10 @@ from unittest.mock import patch import zipfile -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "Scripts")) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) import version -spec = importlib.util.spec_from_file_location("plugin_package", Path(__file__).resolve().parents[1] / "Scripts/package.py") +spec = importlib.util.spec_from_file_location("plugin_package", Path(__file__).resolve().parents[1] / "scripts/package.py") package = importlib.util.module_from_spec(spec) spec.loader.exec_module(package) @@ -40,6 +40,10 @@ def make_repository(self, root): metadata.parent.mkdir(parents=True) metadata.write_text(version.generated("1.2.3")) (repo / "Package.resolved").write_text(json.dumps({"pins": [{"identity": "swift-codex"}]})) + logos = repo / ".github/assets/logo" + logos.mkdir(parents=True) + for name in ("logo-light.png", "logo-dark.png"): + (logos / name).write_bytes(name.encode()) documentation = repo / "Documentation" documentation.mkdir() (documentation / "README.md").write_text("fixture documentation") @@ -57,7 +61,7 @@ def test_version_drift_is_rejected_before_building(self): root = Path(directory) repo, _ = self.make_repository(root) (repo / version.GENERATED).write_text(version.generated("1.2.4")) - with patch.object(package, "__file__", str(repo / "Scripts/package.py")), patch.object(package, "command") as command: + with patch.object(package, "__file__", str(repo / "scripts/package.py")), patch.object(package, "command") as command: with self.assertRaisesRegex(ValueError, "differs from manifest"): package.package(root / "output", "debug") command.assert_not_called() @@ -78,17 +82,34 @@ def command(arguments, cwd): return "1.2.3" return "" - with patch.object(package, "__file__", str(repo / "Scripts/package.py")), patch.object( + with patch.object(package, "__file__", str(repo / "scripts/package.py")), patch.object( package, "command", side_effect=command ): result = package.package(root / "output", "debug") + for entry in repo.rglob("*"): + os.utime(entry, (1700000000, 1700000000)) + repeated = package.package(root / "repeated", "debug") + self.assertEqual(Path(result["archive"]).read_bytes(), Path(repeated["archive"]).read_bytes()) with zipfile.ZipFile(result["archive"]) as archive: + expected = { + "computer-mcp-plugin.toml", "README.md", "CONTRIBUTING.md", "LICENSE", + "THIRD_PARTY_NOTICES.md", "Package.resolved", "Documentation/README.md", + "bin/codex-mcp-adapter", "bin/codex-plugin_CodexAdapter.bundle/schema.json", + "ThirdPartyNotices/swift-codex/LICENSE.txt", "ThirdPartyNotices/swift-codex/NOTICE", + "ThirdPartyNotices/codex-protocol/LICENSE", "ThirdPartyNotices/codex-protocol/NOTICE", + "ThirdPartyNotices.txt", ".github/assets/logo/logo-light.png", ".github/assets/logo/logo-dark.png", + } + self.assertEqual({item.filename for item in archive.infolist() if not item.is_dir()}, expected) + for name in ("logo-light.png", "logo-dark.png"): + relative = ".github/assets/logo/" + name + self.assertEqual(archive.read(relative), (repo / relative).read_bytes()) self.assertEqual(archive.read("computer-mcp-plugin.toml"), manifest) for name in ("LICENSE.txt", "NOTICE"): original = (repo / ".build/checkouts/swift-codex" / name).read_bytes() self.assertEqual(archive.read("ThirdPartyNotices/swift-codex/" + name), original) self.assertIn(original, archive.read("ThirdPartyNotices.txt")) receipt = json.loads(Path(result["receipt"]).read_text()) + self.assertEqual(set(receipt["files"]), expected) self.assertEqual(receipt["architectures"], ["arm64"]) self.assertEqual(receipt["files"]["computer-mcp-plugin.toml"], package.hashlib.sha256(manifest).hexdigest()) @@ -141,7 +162,7 @@ def command(arguments, cwd): manifest.write_bytes(original + b"\n# changed declaration bytes\n") return "" - with patch.object(package, "__file__", str(repo / "Scripts/package.py")), patch.object( + with patch.object(package, "__file__", str(repo / "scripts/package.py")), patch.object( package, "command", side_effect=command ): with self.assertRaises(ValueError): @@ -151,7 +172,7 @@ def command(arguments, cwd): self.assertEqual((repo / "computer-mcp-plugin.toml").read_bytes(), original) def test_package_rejects_links_before_signing_or_running_payload(self): - for relative in ("README.md", "Documentation/link", ".build/debug/codex-plugin_CodexAdapter.bundle/link"): + for relative in ("README.md", ".github/assets/logo/logo-light.png", "Documentation/link", ".build/debug/codex-plugin_CodexAdapter.bundle/link"): with self.subTest(input=relative), tempfile.TemporaryDirectory() as directory: root = Path(directory) repo, built = self.make_repository(root) @@ -161,7 +182,7 @@ def test_package_rejects_links_before_signing_or_running_payload(self): if link.exists(): link.unlink() link.symlink_to(outside) - with patch.object(package, "__file__", str(repo / "Scripts/package.py")), patch.object( + with patch.object(package, "__file__", str(repo / "scripts/package.py")), patch.object( package, "command", side_effect=["", str(built)] ) as commands: with self.assertRaises(ValueError): @@ -187,7 +208,7 @@ def command(arguments, cwd): return "1.2.3" return "" - with patch.object(package, "__file__", str(repo / "Scripts/package.py")), patch.object( + with patch.object(package, "__file__", str(repo / "scripts/package.py")), patch.object( package, "command", side_effect=command ): with self.assertRaises(FileExistsError): diff --git a/Tests/test_package_archive.py b/Tests/test_package_archive.py index cbe57b2..201f92c 100644 --- a/Tests/test_package_archive.py +++ b/Tests/test_package_archive.py @@ -9,7 +9,7 @@ from unittest.mock import patch import zipfile -scripts = Path(__file__).resolve().parents[1] / "Scripts" +scripts = Path(__file__).resolve().parents[1] / "scripts" sys.path.insert(0, str(scripts)) spec = importlib.util.spec_from_file_location("check_package", scripts / "check-package.py") checker = importlib.util.module_from_spec(spec) diff --git a/Tests/test_plugin_release.py b/Tests/test_plugin_release.py index 2cd2ef0..d5e7f00 100644 --- a/Tests/test_plugin_release.py +++ b/Tests/test_plugin_release.py @@ -21,7 +21,7 @@ ROOT = Path(__file__).resolve().parents[1] -SCRIPT = ROOT / "Scripts/plugin_release.py" +SCRIPT = ROOT / "scripts/plugin_release.py" WORKFLOW = ROOT / ".github/workflows/release.yml" spec = importlib.util.spec_from_file_location("plugin_release", SCRIPT) release = importlib.util.module_from_spec(spec) @@ -639,7 +639,7 @@ def test_receipt_validation_rejects_noncanonical_or_invalid_provenance(self): class ReleaseWorkflowTests(unittest.TestCase): def test_job_graph_permissions_and_frozen_output_checks(self): - spec = importlib.util.spec_from_file_location("workflow_check", ROOT / "Scripts/check-workflow.py") + spec = importlib.util.spec_from_file_location("workflow_check", ROOT / "scripts/check-workflow.py") checker = importlib.util.module_from_spec(spec) spec.loader.exec_module(checker) self.assertTrue(checker.check_release_workflow(ROOT)["release_workflow_verified"]) diff --git a/Tests/test_release_gate.py b/Tests/test_release_gate.py index 02956d9..a0bd21b 100644 --- a/Tests/test_release_gate.py +++ b/Tests/test_release_gate.py @@ -12,9 +12,9 @@ def run_gate(self, requirement='exact: "2.3.4"', version="2.3.4", revision="a" * 40, remote=None, remote_status=0): with tempfile.TemporaryDirectory() as directory: root = Path(directory) - scripts = root / "Scripts" + scripts = root / "scripts" scripts.mkdir() - source = Path(__file__).resolve().parents[1] / "Scripts/verify-swift-codex-release-gate.sh" + source = Path(__file__).resolve().parents[1] / "scripts/verify-swift-codex-release-gate.sh" shutil.copy2(source, scripts / source.name) (root / "Package.swift").write_text( '.package(\n url: "https://github.com/swift-library/swift-codex.git",\n ' diff --git a/Tests/test_version.py b/Tests/test_version.py index 5350063..2bdd144 100644 --- a/Tests/test_version.py +++ b/Tests/test_version.py @@ -6,7 +6,7 @@ import unittest from unittest.mock import patch -SPEC = importlib.util.spec_from_file_location("plugin_version", Path(__file__).resolve().parents[1] / "Scripts/version.py") +SPEC = importlib.util.spec_from_file_location("plugin_version", Path(__file__).resolve().parents[1] / "scripts/version.py") version = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(version) diff --git a/Scripts/check-host.py b/scripts/check-host.py similarity index 100% rename from Scripts/check-host.py rename to scripts/check-host.py diff --git a/Scripts/check-package.py b/scripts/check-package.py similarity index 100% rename from Scripts/check-package.py rename to scripts/check-package.py diff --git a/Scripts/check-workflow.py b/scripts/check-workflow.py similarity index 99% rename from Scripts/check-workflow.py rename to scripts/check-workflow.py index 9f0bae5..421a216 100644 --- a/Scripts/check-workflow.py +++ b/scripts/check-workflow.py @@ -40,14 +40,14 @@ def require(condition, message): "Verification cannot receive secrets or write permission") require("ref: ${{ github.workflow_sha }}" in verify and "persist-credentials: false" in verify, "Verification must execute the workflow revision without persisted credentials") - require("Scripts/plugin_release.py verify" in verify and "--publish" in verify, + require("scripts/plugin_release.py verify" in verify and "--publish" in verify, "Verification must assemble both publication and dry-run bundles") require(" if: inputs.publish\n needs: verify\n" in publish and " permissions:\n contents: write\n" in publish, "Only publication may write after verification") require("actions: read" not in publish and "secrets" not in publish and "actions/checkout@" not in publish - and not re.search(r"swift\s+(build|test)|Scripts/package.py", publish), + and not re.search(r"swift\s+(build|test)|scripts/package.py", publish), "Publication cannot build or receive catalog secrets") for field in ("artifact_id", "asset_digests", "helper_digest"): require("needs.verify.outputs." + field in publish, "Publication must use frozen verification outputs") diff --git a/Scripts/import-schema.mjs b/scripts/import-schema.mjs similarity index 98% rename from Scripts/import-schema.mjs rename to scripts/import-schema.mjs index bac0626..7214fa6 100644 --- a/Scripts/import-schema.mjs +++ b/scripts/import-schema.mjs @@ -98,7 +98,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1] const check = args.includes('--check'); const positional = args.filter(value => value !== '--check'); if (positional.length > 1 || positional.some(value => value.startsWith('--'))) { - throw new Error('Usage: node Scripts/import-schema.mjs [SDK_REPOSITORY] [--check]'); + throw new Error('Usage: node scripts/import-schema.mjs [SDK_REPOSITORY] [--check]'); } const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const sdk = resolve(positional[0] ?? join(root, '.build/checkouts/swift-codex')); diff --git a/Scripts/package.py b/scripts/package.py similarity index 93% rename from Scripts/package.py rename to scripts/package.py index b63f193..22622a0 100644 --- a/Scripts/package.py +++ b/scripts/package.py @@ -138,6 +138,10 @@ def package(output, configuration): copy_tree(bundle, binary_directory / bundle.name) for name in ["computer-mcp-plugin.toml", "README.md", "CONTRIBUTING.md", "LICENSE", "THIRD_PARTY_NOTICES.md", "Package.resolved"]: copy_file(repo / name, payload / name) + logos = payload / ".github/assets/logo" + logos.mkdir(parents=True) + for name in ("logo-light.png", "logo-dark.png"): + copy_file(repo / ".github/assets/logo" / name, logos / name) copy_tree(repo / "Documentation", payload / "Documentation") notices = payload / "ThirdPartyNotices" notices.mkdir() @@ -197,7 +201,14 @@ def package(output, configuration): archive = stage / archive_name with zipfile.ZipFile(archive, "x", compression=zipfile.ZIP_DEFLATED) as package_zip: for entry in sorted(payload.rglob("*")): - package_zip.write(entry, entry.relative_to(payload).as_posix()) + name = entry.relative_to(payload).as_posix() + directory = entry.is_dir() + info = zipfile.ZipInfo(name + "/" if directory else name, (1980, 1, 1, 0, 0, 0)) + info.create_system = 3 + mode = 0o40755 if directory else (0o100755 if entry.stat().st_mode & 0o111 else 0o100644) + info.external_attr = (mode << 16) | (0x10 if directory else 0) + info.compress_type = zipfile.ZIP_DEFLATED + package_zip.writestr(info, b"" if directory else entry.read_bytes()) receipt = {"plugin_id": "codex", "configuration": configuration, "architectures": architectures, "platform": platform, "signature": "ad-hoc", "publisher_verified": False, "published": False, diff --git a/Scripts/plugin_release.py b/scripts/plugin_release.py similarity index 100% rename from Scripts/plugin_release.py rename to scripts/plugin_release.py diff --git a/Scripts/verify-swift-codex-release-gate.sh b/scripts/verify-swift-codex-release-gate.sh similarity index 100% rename from Scripts/verify-swift-codex-release-gate.sh rename to scripts/verify-swift-codex-release-gate.sh diff --git a/Scripts/version.py b/scripts/version.py similarity index 98% rename from Scripts/version.py rename to scripts/version.py index 1be34a0..029d29a 100644 --- a/Scripts/version.py +++ b/scripts/version.py @@ -26,7 +26,7 @@ def read(root): def generated(value): - return f'''// Generated by Scripts/version.py from computer-mcp-plugin.toml. + return f'''// Generated by scripts/version.py from computer-mcp-plugin.toml. package enum CodexAdapterBuildInfo {{ package static let version = "{value}" }} @@ -59,7 +59,7 @@ def comparison_base(root, base): def check(root, binary=None, base=None, tag=None): value = read(root) if (root / GENERATED).read_text() != generated(value): - raise ValueError("Adapter metadata differs from manifest; run Scripts/version.py generate") + raise ValueError("Adapter metadata differs from manifest; run scripts/version.py generate") if binary: result = subprocess.run([str(binary.resolve()), "--version"], capture_output=True, text=True, timeout=10, check=True) if result.stdout.strip() != value: