Repository navigation
Release v1.0.1: MCP write authority comes from the person, plus the agent test layer - #42
Merged
Merged
Conversation
The distributed v1.0.0 let an AI client change a target, or load the runtime into it, without the person having allowed writes, through mutation_permission (a boolean the client wrote itself), batch_run, cortex_attach and cortex/private/route. - cortex.exe mcp takes --allow-writes; without it every control/mutate/native call is refused (write_authority_required). The authority travels on the authenticated pipe envelope; mutation_permission stays as declared intent. - batch_run is classified from its ops against an allowlist, re-checked after $from_step resolution; semantic steps use the same gate. - cortex_attach does not inject without authority; only initialize, ping, server/discover, tools/list, tools/call and notifications/* are forwarded; the private route is accepted only from the desktop adapter. - scan_* with pause_process and prompt_* are control operations. - A tool no rule classifies is refused (tool_not_classified) instead of defaulting to analyze; tests/fixtures/mcp_tool_risks.json records the class of every primitive. - Refused calls are logged to cortex_denied_mutations.jsonl; debugger attach/detach is journaled (AI Activity + cortex_debugger_events.jsonl). - Semantic results no longer carry confidence/hypothesis fields. - tests/security_no_writes_regression.py calls every listed tool with mutation_permission=true and requires an unchanged target state hash; tests/run_security_regression_vm.ps1 replays it on Windows without --skip-tool. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018noSWjr7FkDDFzzYZ5XS9u
- The native authority test removed its log directory while its own ifstream was still open; Windows refuses that. Close the file first. - The GUI prompt test started `cortex.exe mcp` without --allow-writes and called prompt_value_change without mutation_permission. Prompt tools are control operations now, so the suite (and the imgui-ui workflow) pass both, as a person configuring a client would. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018noSWjr7FkDDFzzYZ5XS9u
…equest - README, docs/mcp.md, docs/getting-started.md and agent/agents.md show the read-only client configuration and the one with --allow-writes, and say that a v1.0.0 client that changed a target must now pass the flag. - CHANGELOG: propose 1.0.1 and mark the change as breaking for writing clients. - docs/mcp.md records why cortex_operation_cancel needs no write authority. - The five Windows workflows run on every pull request (no path filter) and on pushes to main, so a regression of this kind no longer needs a manual run. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018noSWjr7FkDDFzzYZ5XS9u
…file The previous fix scoped them inside the block, so the test no longer compiled. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018noSWjr7FkDDFzzYZ5XS9u
ReadTargetThreadRegisters suspended a thread, read its context, then closed the thread handle before the guard that resumes it ran. ResumeThread on a closed handle fails, so the thread stayed suspended: any call to debug_registers froze the target (found by the regression test; reproduced under Wine, seen on Windows CI). Resume explicitly before CloseHandle. The regression test now blames a stall on the exact call (it waits for one state publish after every call), no longer crashes when the target is frozen, runs the x86 target even if x64 fails, and has an --isolate mode, run by CI after a failure, that calls each tool on its own fresh target. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018noSWjr7FkDDFzzYZ5XS9u
main gained the agent test layer (launch profiles, cortex_test_*, persistent knowledge graph, 37 new tools). Conflicts were in mcp_bridge/host_mode.cpp and agent/agents.md. Resolution: - both option sets kept (--allow-writes and --launch-config/--test-results); - cortex_launch(attach) may load the runtime because the operator-owned profile already granted allow_attach; - the new host tools carry a class in tools/list (read_only, or host_control with an effect), the regression test requires it, and the fixture lists the six new project_knowledge_* primitives; - docs/CHANGELOG state that the journal/knowledge-writing host tools are not yet behind --allow-writes. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018noSWjr7FkDDFzzYZ5XS9u
Saving project notes, addresses, pointer paths, knowledge, facts and the test journals/reports does not touch the target, but the content comes from the client and a later session reads it back. It is now its own risk class, `state`, which needs the caller's mutation_permission AND the person's --allow-state (or --allow-writes, which includes it). --allow-state alone still refuses every write to the target. - runtime: ToolRisk::State, Authority::stateAllowed (envelope field state_allowed), error state_authority_required; 11 project/fact tools move from control to state. - host: --allow-state; the cortex_test_* tools that persist journals, reports or knowledge are checked against it. - cortex_launch(attach) injected with full write authority because of how the merge resolved CreateRuntime; injection and write authority are now separate arguments, and the launch profile grants injection only. - regression test: the sweep checks the refusals, and a new control run with --allow-state proves notes are saved, the host journal tool passes the authority check, the target cannot be written and its state hash is unchanged. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018noSWjr7FkDDFzzYZ5XS9u
…w-state mcp_schema_tests expected project_knowledge_put/verify to be control after the merge of main; they are state (the person's --allow-state, not --allow-writes). test_runner_e2e.py persists knowledge, the test catalog and reports through project_knowledge_* and cortex_test_*, so both of its MCP clients now start with --allow-state. It still gets no --allow-writes: launching, input and attach stay with the operator-owned launch profile. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018noSWjr7FkDDFzzYZ5XS9u
…gate test_runner_e2e.py arms hardware-write log breakpoints on the target (debug_breakpoint_add/delete). That changes the target, so the person must grant --allow-writes; with --allow-state alone it is refused, as the Windows CI run of 13e0d74 showed (write_authority_required at the first breakpoint). - the main client starts with --allow-writes (includes --allow-state); - a first client with --allow-state only launches and attaches through the operator-owned profile and asserts that arming a breakpoint is refused, so "a launch profile may attach the runtime but grants no write authority" stays tested (it was broken once by a merge resolution); - the reconnect client keeps --allow-state: it only reads and records. Run end to end under Wine (windows debugger backend): 86 checks pass. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018noSWjr7FkDDFzzYZ5XS9u
The host tool that sends keys and mouse clicks to the launched program carried
{"read_only": true} in tools/list: an if / else-if / else chain overwrote the
target_input metadata set just before it. Only the label was wrong (the
mutation_permission check and the operator-owned launch profile apply when the
tool is called), but the label is what a client or a reviewer reads.
It is now host_control with effect target_input and mutation_permission
required. The security regression test also checks the label of five
representative host tools (cortex_test_run, cortex_test_cancel,
cortex_test_catalog_save, cortex_test_link, cortex_test_report), so a label that
is present but wrong fails it; the earlier check only required some label.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018noSWjr7FkDDFzzYZ5XS9u
… test layer Security release. A client that changes a target must now start cortex.exe mcp with --allow-writes; a client that only saves Cortex's own records needs the new --allow-state; observing and analyzing clients are unaffected. It also publishes the agent test layer merged into main after v1.0.0 (launch profiles, bounded input trials, a persistent knowledge graph: 37 new tools, 251 on the full profile). - Version 1.0.1 in cortex --version, the About text and the MCP server version. - CHANGELOG: full v1.0.1 section (Security, Added, Changed, Fixed, Tests and CI). - README, README_INSTALL.txt and the docs name cortex-v1.0.1-windows-portable.zip. - publish-release.yml publishes v1.0.1, and when the commit message contains "Release v1.0.1" (a merge commit of a pull request with that title qualifies, not only a direct commit). Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018noSWjr7FkDDFzzYZ5XS9u
imgui-ui.yml still matched "1\.0\.0" against `cortex --version`, which now answers "cortex 1.0.1"; the CLI compatibility smoke of the windows-x64 job failed on it (run 38883616202). The binary was right, the check was stale. My first search for version strings missed it because the dots are escaped in the regex; a search for the escaped form finds no other occurrence. This commit message starts with the release marker on purpose: publish-release reads the message of the head commit of the push to main. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018noSWjr7FkDDFzzYZ5XS9u
PdB333
pushed a commit
that referenced
this pull request
Oct 16, 2026
…een) - PREREGISTRATION: the pin moves from 29c2ea3 to fee9c60, the head of the release pull request (#42), with the run ids of the seven green workflows and the state hashes of the no-write regression (x64 422b2c886ff27cfd, x86 848f9ca4848caa2e, unchanged). - cortex_tool_lists.json regenerated at that commit: identical in names, counts and classes to the lists at 29c2ea3 (251 tools; C 137, D0 72, D0' 93). Between the two commits only the version string and the tools/list label of cortex_test_run changed. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018noSWjr7FkDDFzzYZ5XS9u
Owner
Author
|
CI:
Generated by Claude Code |
…e authority The desktop keeps the person's Writes allowed state as one shared flag on the payload client. It is set once per frame (desktop_ui.cpp), and my authority change also set it inside RuntimeModelBase::Call and ReModel::Call from the `mutationAllowed` argument. The read-only overload passes mutationAllowed=false whatever the switch says, so every read-only model call lowered the flag. The debugger's own breakpoint calls (DebuggerModel goes to the payload directly) then reached the runtime with writes_allowed=false and were refused: with Writes allowed on, "find what accesses" could record nothing. Windows CI caught it once (windows-release-full, portable pass, test access_finder_recorded_no_write, PR run 38905331962). It did not show on the earlier runs of the same commit because it depends on whether a read-only model call runs between the frame start and the breakpoint call. Reproduced under Wine with the build of fee9c60: the GUI attached suite fails 4 of 4 runs with access_finder_recorded_no_write. With this change it passes 3 of 3 (frame_writes=16). Only the per-frame sync lowers the authority now; a mutating call, which got past the switch check, raises it. app_models_tests encoded the old behaviour (a read-only call was expected to drop the authority); it now asserts the reverse, and fails on the previous code. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018noSWjr7FkDDFzzYZ5XS9u
Owner
Author
|
Correction to my previous comment: the I wrote that nothing in this change was on the access-finder path. That was wrong: the desktop's write authority is on it.
Generated by Claude Code |
PdB333
pushed a commit
that referenced
this pull request
Oct 16, 2026
- PREREGISTRATION: the pin is the release commit 91f5a22 (pull request #42), whose tree is identical to the validated head 34ce4dd. Eight Windows workflows green on that head; state hashes of the no-write regression unchanged (x64 00fe1d6cc8b090dd, x86 6151f471602f88dc). - cortex_tool_lists.json regenerated at that commit: identical in names, counts and classes to the lists at fee9c60 (251 tools; C 137, D0 72, D0' 93). - main is merged into this branch, so it now contains the released code. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018noSWjr7FkDDFzzYZ5XS9u
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
How this publishes
publish-release.ymlruns on the push tomainand publishes when the head commit message containsRelease v1.0.1. A merge commit of a pull request carries the PR title in its message, so "Create a merge commit" (the usual way in this repository) publishes as long as the title above is kept; squash does too; "Rebase and merge" also does, because the last commit of the branch starts withRelease v1.0.1.It then builds x64 and x86, assembles
cortex-v1.0.1-windows-portable.zip(README, README_INSTALL.txt, CHANGELOG.md, LICENSE), checks thatcortex --versionanswers 1.0.1, takes the release notes from the## [v1.0.1]section ofCHANGELOG.md, and creates the tag and the GitHub release. It refuses to overwrite an existingv1.0.1release.The same workflow ran on this PR's final head (its
pull_requestrun builds the archive and skips the publish step): run 38910602137.Summary
Security release. The distributed v1.0.0 let an AI client change a target, or load the runtime into it, without the person having allowed writes, through
mutation_permission(a boolean the client wrote itself),batch_run,cortex_attachandcortex/private/route. This moves the decision outside the model, adds a narrower tier for Cortex's own records, and adds a regression test that fails if any listed tool can change a target without that decision. It also publishes the agent test layer merged intomainafter v1.0.0 (pull requests #17 to #39: launch profiles, bounded input trials, a persistent knowledge graph; 37 new tools).Breaking for MCP clients that write:
cortex.exe mcp --allow-writes;cortex_test_*journals/catalog/reports/links) needs--allow-state, which cannot change the target;--allow-writesincludes it;README,
docs/mcp.md,docs/getting-started.mdandagent/agents.mdshow the three configurations. The version is 1.0.1 (a minor number such as 1.1.0 would also fit 37 new tools; it is a text replacement if preferred).What changes
--allow-state= also save Cortex's records (state_authority_required).--allow-writes= also change the target or load the runtime (write_authority_required; includes state). Both flags are set by the person and reach the runtime on the authenticated pipe envelope (writes_allowed,state_allowed), never inside a tool call. The desktop supplies its own authority from the Read-only / Writes allowed switch (synced once per frame).mutation_permission=truestays as the caller's declared intent: required, no longer sufficient.batch_runis classified from its operations against an allowlist; unknown or computed operations are refused, re-checked after$from_stepresolution.cortex_attachno longer injects the runtime without authority; the server forwards onlyinitialize,ping,server/discover,tools/list,tools/callandnotifications/*; the private desktop route is accepted only from the desktop adapter.tool_not_classified).tests/fixtures/mcp_tool_risks.jsonrecords the class of all 179 primitives.scan_*withpause_processandprompt_*are control operations; 11 project/fact tools are thestateclass.tools/list(read_only, orhost_controlwith aneffectoftarget_input,cortex_stateortrialfor the ones that act). The operator-owned launch profile (--launch-config) stays the authority for launch, restart, stop, attach and test input; a launch profile that attaches the runtime grants no write authority (tested).cortex_denied_mutations.jsonl; debugger attach/detach are journaled (AI Activity tab andcortex_debugger_events.jsonl).confidence,evidence_confidenceor hypothesis fields that Cortex never computed.cortex --version, the About text and the MCP server version; README, README_INSTALL.txt and the docs namecortex-v1.0.1-windows-portable.zip;publish-release.ymland the CLI smoke inimgui-ui.ymlexpect 1.0.1; CHANGELOG has a full## [v1.0.1]section.Three bugs found by the work on this branch
debug_registersfroze the target.ReadTargetThreadRegistersclosed the thread handle before the guard that resumes it ran, so the thread stayed suspended. Fixed incore/target/thread_provider.cpp; found by the regression test.cortex_test_runwas listed asread_onlyintools/list(an if / else-if / else chain overwrote itstarget_inputlabel). Only the label was wrong: the permission and the launch profile were enforced at call time. Fixed; the regression test now checks the label of five representative host tools.RuntimeModelBase::Call/ReModel::Call, and the read-only overload passesmutationAllowed = false, so every read-only model call lowered it. The debugger's direct breakpoint calls then reached the runtime withwrites_allowed=falseand were refused: with Writes allowed on, "find what accesses" could record nothing (GUI suite testaccess_finder_recorded_no_write, failed once in the PR's run 38905331962 and not on earlier runs of the same commit, because it depends on whether a read-only model call runs between the frame start and the breakpoint call). Reproduced under Wine (4 of 4 failures on the build offee9c60, 3 of 3 passes with the fix). Fixed in34ce4dd: only the per-frame sync lowers the authority; a mutating call raises it.tests/app_models_tests.cpphad encoded the old behaviour and now asserts the reverse (it fails on the previous code).Evidence (Windows CI, head
34ce4dd48301571ab13bdadb54a259891f161e6c, eight workflows green on the pull request)tests/security_no_writes_regression.py, with no--skip-tool, starts the test target, loads the runtime beforehand, startscortex.exe mcpwithout--allow-writes, calls every tooltools/listreturns (251) withmutation_permission=true, and compares the target's code-and-data hash before and after. Step "Without --allow-writes no MCP call changes a target", jobwindows-release-fullof run 38910602459:00fe1d6cc8b090dd00fe1d6cc8b090dd6151f471602f88dc6151f471602f88dcControls in the same run: with
--allow-writesandmutation_permissionthe write is applied and the hash changes; with--allow-writesbut nomutation_permissionit is refused; with--allow-statea note is saved, a host journal tool passes the authority check, the target still cannot be written (write_authority_required) and its hash is unchanged;batch_runwrites, unknown ops, computed op names, semanticbatch_runand a rawcortex/private/routeare refused;cortex_attachwithout authority loads nothing (no token file); the target survivescortex_detach. No stall warning in this run. The GUI suite passes in both of its passes in the same job (the access finder records its writes:frame_writes=5and7).Runs on the head: imgui-gui-tests 38910602459, contracts 38910602153, imgui-ui 38910602164, unified-host 38910602279, e2e-windows 38910602150, full-windows-build 38910602249 (x64 and x86, including "Test bounded input trials and reports"), publish-release 38910602137, ui-migration-benchmarks 38910602201.
History, so the evidence is not read as a single pass:
8d20d2cwas green on five workflows beforemainwas merged;22a55f6(merge +--allow-state) hadcontractsred (a stale test expectingcontrolfor knowledge writes);13e0d74hadfull-windows-buildred (the runner e2e arms breakpoints, which correctly needs--allow-writes);29c2ea3was green on six;752568e(release commit) hadimgui-uired because its CLI smoke still expected1.0.0;fee9c60was green on seven when run by hand and then failed once on the PR (the authority regression above);34ce4ddfixes it and is green on all eight.Known limits (not fixed here)
docs/mcp.md. An authority granted by the desktop for a limited time is planned, not built.debug_*read tools stay outside the control class, on purpose. They write no memory, but a debugger attached to a process suspends its threads; "read-only" means "no write to memory", not "no effect on the target". Attach/detach are journaled and counted. A separate--allow-debugis not built.cortex_operation_cancelandcortex_test_cancelneed no authority (they only cancel something already authorized).docs/mcp.mddocuments this forcortex_operation_cancel;cortex_test_cancelis labelledtrialintools/listand tested, but not yet named in the docs.lua_scripts_*/ocr,capture_runtime_state, 2.6 s afterwatch_allocations_*); the target recovered each time and its hash is unchanged, so they read as tool latency, not a freeze; they were not isolated with--isolate. The last two runs had none.tests/semantic_plan_p1_tests.cppis stale (it describes the plan-only phase) and is run by no workflow; it is untouched.For the open pull requests
New host or primitive tools added by other branches (for example #40 and #41) must carry a label in
tools/listand, for primitives, a class intests/fixtures/mcp_tool_risks.json, orsecurity_no_writes_regression.pyandtool_not_classifiedwill reject them; the tools that save Cortex's records belong to thestateclass.🤖 Generated with Claude Code
https://claude.ai/code/session_018noSWjr7FkDDFzzYZ5XS9u