Skip to content

fix(security): harden credential, UI and atomic write boundaries - #126

Closed
enqiang wants to merge 2 commits into
aoci-spec:mainfrom
enqiang:codex/security-boundaries-20261014
Closed

enqiang wants to merge 2 commits into
aoci-spec:mainfrom
enqiang:codex/security-boundaries-20261014

Conversation

@enqiang

@enqiang enqiang commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Problem and resulting behavior

Four boundaries could disclose credentials or weaken guarded writes. This change:

  • publishes host-configuration backups with owner-only permissions;
  • refuses model-request redirects and credential-bearing/query/fragment endpoint URLs, while retaining error categories and HTTP status without persisting arbitrary provider bodies or formatted transport causes;
  • requires a fresh per-process capability for status-page repository data, including HEAD and conditional requests, and keeps registrations private;
  • requires native Linux atomic exchange for new governed replacements instead of the unsafe link/rename fallback, while retaining existing capture-artifact recovery readers.

The follow-up regression verifies that replacing a pre-existing public-mode status-page registration changes it to mode 0600 and preserves the new process/capability binding.

Compatibility and scope

The binary remains aoci; the nine MCP tools, input schemas, formal cognition formats, and transaction identity derivation are unchanged. No dependency is added or upgraded.

  • Readiness links carry a capability in the URL fragment. Legacy pages remain stoppable, but must be stopped before a new detach replaces them. Stop new pages with their originating binary before downgrading.
  • Model endpoints must be configured at their final HTTP(S) base URL. URLs using redirect-based routing or query-string authentication need adjustment.
  • Linux filesystems without RENAME_EXCHANGE, including WSL DrvFs, refuse governed CAS replacement. Use a supporting filesystem; existing recovery artifacts must be retained.
  • Previously written backups and provider-error artifacts are not automatically scrubbed. Windows directory ACLs remain the confidentiality boundary.

Validation

Follow-up head: 6c1b5d309888751c5a334bf97be94f386559e864.

  • GitHub Fast CI: 8/8 jobs passed, including the previously failed cli-and-static job: https://github.com/aoci-spec/aoci-code/actions/runs/38589075022
  • make fast, full package tests, staticcheck, public-text safety, and the complete go test -race -count=1 -timeout=30m ./... passed. Package concurrency was bounded with GOFLAGS=-p=2; no project timeout was changed.
  • MCP conformance: 53 passed, 0 failed.
  • Fault-injection scenarios: 75 passed, 0 failed.
  • Complete deterministic lifecycle: 87 PASS, 3 CHAR, 0 FAIL.
  • Self upgrade axis: 80 passed, 0 failed; the test ladder reported 0 overdue contracts.
  • Previously blocked live-database lane: real MySQL 8.4.11, 19 passed, 0 failed in a separate database-only run. It also passed inside the complete lifecycle run. An isolated Docker data/runtime directory supplied the fixture without deleting existing containers, images, or volumes.
  • Connector origin/checksum verification and exact patch replay, clean-room smoke, and git diff --check passed.
  • govulncheck: 0 reachable vulnerabilities; 26 module-level advisories were reported as not called.
  • The tagged database-integration command passed; unconfigured live-engine lanes are not claimed as executed. MySQL lifecycle acceptance is the explicit live-engine evidence above.
  • AOCI Observe review advanced only the changed regression test's Baseline fingerprint; Verify and Check passed and Guide reported complete, with no pending recovery.

Remaining verification limitation

The local make verify invocation ran the full gate collection and black-box suites, but its aggregate is not green: the license gate could not resolve six golang.org/x/* license-source URLs because external metadata requests timed out and returned Unknown. The gate was not weakened or skipped. Module checksums, connector provenance and legal metadata checks passed; dependencies are unchanged by this PR. The earlier validation's six-platform license pass remains recorded, but this later failed attempt is not presented as a pass. Recheck that gate on a runner with working access to the Go metadata endpoints before requiring a fully green local closure.

The original failed upstream CI attempt stopped while downloading staticcheck from the Go checksum service; the new head's complete Fast CI run passed.

Platform coverage

macOS local tests and black-box suites passed. Linux native-exchange and unsupported-exchange injection/recovery checks were exercised in the original security validation; the opt-in real-filesystem probe was not configured. Windows cross-build and CI protocol conformance passed; native Windows ACL behavior was not tested locally.

@enqiang
enqiang marked this pull request as ready for review October 14, 2026 15:46
alkor2000 pushed a commit that referenced this pull request Oct 14, 2026
… capability for the status page

Three boundaries from PR #126 by enqiang, landed with the exchange
fallback removal left out:

- Host-configuration backups written by the installers were published
  with the generic new-file mode; a .mcp.json or config.toml backup can
  carry credentials for unrelated servers, so backups are now owner-only
  through the new fs.AtomicWriteMode, independent of Git ignore rules.
- The optional model client refuses redirects (a caller-owned HTTP client
  keeps its own policy outside the authoring client), refuses base URLs
  carrying user information, a query string, or a fragment, and reports
  only the HTTP status and error category: endpoint error bodies and
  transport causes are no longer formatted into errors that callers log
  or persist, including the failure notes of endpoint-native drafting.
- The loopback status page requires a fresh per-process capability for
  every data route, including HEAD and conditional requests. The
  readiness URL carries it in its fragment, the browser keeps it in tab
  session storage and sends it in an Authorization header, the static
  shell holds no repository data, registrations are written owner-only,
  and a live tokenless page from an older binary is refused by a new
  --detach until aoci ui --stop ends it.

Not taken: the PR also replaced the RENAME_EXCHANGE link-and-rename
emulation with a fail-closed refusal, which would make every governed
write on WSL DrvFs fail again (#116). The emulation stays, with its
documented link-to-rename window; the CHANGELOG, spec, SECURITY.md, and
troubleshooting text about it is unchanged here.

Landed from PR #126 by enqiang with its governance files dropped; the
maintainer regenerates the Baseline and cognition Entries in the closing
commit of this batch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@alkor2000

Copy link
Copy Markdown
Contributor

Three of the four boundaries are on main as 952397c, authored by you. The exchange-fallback removal is not, and here is why.

Taken as-is: owner-only host-config backups (with the new fs.AtomicWriteMode), the model-client hardening (no redirects, no credential-bearing or query/fragment base URLs, no endpoint bodies or transport causes in errors or persisted notes), and the status-page capability (fragment token, Authorization header on every data route including HEAD and conditionals, private registrations, a legacy page refused by a new --detach). Your tests and the scenario-suite change came along unchanged.

Not taken: replacing the RENAME_EXCHANGE link-and-rename emulation with a refusal. That emulation exists because every governed write on WSL DrvFs (/mnt/*) failed before it (#116), and refusing again sends those users back to a tool that cannot write at all. The window you describe is real and was already written down in the CHANGELOG and in the file header: a writer outside aoci that replaces the target by rename between the link and the first rename is overwritten rather than detected, in-place modification is still caught, and every aoci writer is serialized by the CAS lock. A documented link-to-rename window on a filesystem that cannot do atomic exchange at all seemed better to us than no governed writes on it. If you have a concrete scenario where a cooperating process loses data through that window, open an issue with it and we will look again; an opt-in strict mode that refuses the emulation is something we would take.

A pre-tag review of the landed tree turned up three things worth fixing on top of it, now on main as 81290f9: a page opened without the fragment (pasted link, stale tab, or --open on a system that drops #...) was blank with no hint, so it now shows which link to open; aoci ai test and aoci doctor --net had lost every transport detail, so the hint now names a sanitized cause class (dns, refused, tls, timeout, redirect) while the raw cause still never reaches the message; and a foreground aoci ui could still replace a live tokenless page's registration, so it is refused the same way --detach is.

The governance files were dropped as usual and regenerated in the closing commit. Ships in rc21.

Release: https://github.com/aoci-spec/aoci-code/releases/tag/v0.1.0-rc21

@alkor2000 alkor2000 closed this Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants