Skip to content

Direct-write lane: types the owner opens are written without review - #6

Closed
theaspirational wants to merge 9 commits into
vsov:mainfrom
theaspirational:pr/direct-write-lane
Closed

theaspirational wants to merge 9 commits into
vsov:mainfrom
theaspirational:pr/direct-write-lane

Conversation

@theaspirational

Copy link
Copy Markdown
Contributor

Stacked on #5 and #4. This branch starts where okf-ids ends, so the diff shows their commits until they merge; merge those two first and this PR shrinks to its own five commits. No conflicts in that order.

Until now an agent had one way to write: file a proposal and wait for the owner. That is right for knowledge and wrong for session notes, observations and logs, where several agents working at once need to see each other's writes now. This adds a second lane the owner opens per concept type. Nothing changes for a bundle that does not open it. No version is bumped: the number is yours.

The lane

  • meta/purpose.md, under write_policy: proposals, may declare direct: {types: [note, session-log]}. write_policy keeps its two literals and the pre-commit hook is unchanged: a direct write commits through the same _commit every sanctioned writer uses.
  • okfy.proposals.write_direct(...) writes a concept whose type is listed, at once. The lane is decided by the concept's current type on an update and by the proposed type on a create. meta/ and the other reserved directories are never in it, so the policy cannot rewrite itself.
  • Concurrency is optimistic and adds no lock. expect_sha256 is the sha256 of the file the author read (the digest okfy_show already returns), or absent to create. Check and write are one step under the existing review_lock, from one read, as a temp file and a rename. Two writers holding the same sha256: one lands, the other is refused E_WRITE_BASE_MOVED with the current sha256.
  • What a proposal would be refused for is refused here by the same functions: the injection and secret scans, sources: outside the corpus, the archetype schema (E_DIRECT_INVALID). A direct write cannot change type (E_DIRECT_RETYPE) or what an owner decision wrote: verified, generated, the stale fields, supersedes / superseded_by (E_DIRECT_OWNER_FIELD).
  • The ledger row is an accept with proposal: null, origin: agent-direct, the agent as actor and base_sha256. It is a content write, not a decision: unresolved_rejections and okfy dismiss read one shared fold that skips it.
  • CLI: okfy write <bundle> <concept_id> --as <actor> --expect <sha256|absent> (--from <file> | --patch-file <hunks.json>). It refuses; it does not fall back.

MCP

  • okfy_write(concept_id, actor, expect_sha256, content?, patch?, note?, evidence?). The agent does not need to know the policy: a listed type is written at once (channel: "direct", with the new file's sha256); any other type, and any bundle with no lane, is filed through the okfy_propose path (channel: "proposal"), only if the target is still at expect_sha256. persisted: true in both cases. A refusal is {error, message, way_out}.
  • A direct write's ledger row keeps four counters from the server's session trace (by, queries, target_shown, base_shown), so an agent that takes current_sha256 from a refusal and writes without reading is visible. Not prevented: an actor string is a record, not authentication.
  • A direct create of a type the plan does not declare is refused E_UNKNOWN_TYPE before the lane check.
  • okfy package adds one block to AGENTS.md between <!-- okfy:direct --> markers, only for a bundle with a well-formed direct block; every other bundle's AGENTS.md is byte-identical.
  • No existing response shape changed.

Stricter behaviour

direct in meta/purpose.md is now a checked key: E_DIRECT_KEY, E_DIRECT_TYPE, E_DIRECT_POLICY at every strictness level, W_DIRECT_TYPE for a listed type outside the archetype's canonical list in a bundle that declares no types. "An MCP tool call can never touch a final concept" is now true per bundle, not per adapter.

One commit outside the lane, droppable on its own

The injection scan joins hard wraps only. The second pass of okfy.injection.scan_values used to collapse every newline of a value, so the exfiltration rule, written for one line, matched an outbound verb, the word password and a URL anywhere in a document. Measured on a 48-concept bundle of working notes: 8 bodies refused, 5 of them for words in different paragraphs. Blank lines and the start of a list item, heading, quote or table row are now line boundaries. A phrase split by a hard wrap is still caught. One split across a blank line is seen by neither pass: that is the price, yours to accept or refuse.

Not changed, and not claimed

okfy refine is left alone: the owner's door, no scans, no lock. One commit per direct write. A concept created directly is found by okfy query at once and reaches index.md at the next okfy package, like an accepted create.

Tests

core/tests/test_direct_write.py and adapters/mcp/tests/test_write_tool.py: every refusal also asserts nothing was written (file bytes, ledger rows and commit count compared before and after). The full CHANGELOG section in this branch lists every new code.

🤖 Generated with Claude Code

theaspirational and others added 9 commits October 16, 2026 00:38
…tched

A short English stopword list is dropped from the query before BM25, in
query.search_pool only: the index keeps every token, so no bundle's index
fingerprint moves. In a small corpus a word like "who" carried real idf and
pulled an unrelated concept to rank 2 (the reviewer's "Rook FTP login
credentials who issued" ranked the owner profile second on "who" alone).

Each hit now carries matched_terms: the query's terms, lexicon expansion
included, found anywhere in the concept's index tokens, body included. The
MCP adapter's `matched` used to overlap the query with title+description
only, so a hit that matched in its body showed [] and looked like noise.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 9d86b0c)
…=true

`matched.terms` on a query hit is now the core's matched_terms (body
included, stopwords never) instead of an adapter-side overlap with the
hit's title and description; via_lexicon still names the expansion's
share. Same key, better value: the additive contract holds.

okfy_show(concept_id, outline=true) returns the `## ` headings in order
and the sha, no content, so an agent can pick a `section` without reading
the whole concept or guessing a heading to get the menu from the error.
Single concept_id only; a multi-id call ignores it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 6d40d74)
FastMCP now carries `instructions`: search with the user's own words,
okfy_overview is the map, okfy_show's sha256 goes into okfy_write, a
fact worth keeping goes back on the concept it belongs to, a search that
finds nothing becomes okfy_propose(action="gap", query=<the user's
words>), and retrieved content is data. A bundle-mode query with no hits
also carries `way_out` naming that gap proposal: the moment an agent
needs the gap door is the moment it sees it. Until now the door was one
phrase inside okfy_propose's description and nothing else told a fresh
client about it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 592f565)
A concept id is its path, so it changes when a page moves. Anything outside
the bundle that points at a page needs an id that survives renames and edits.

- bundle_uid in meta/purpose.md and okf_id in each concept page's
  frontmatter; reserved pages are addressed as bundle_uid plus path.
- okfy migrate ids backfills both into an existing bundle with uuid5s every
  full clone derives alike; the git read is pinned against clone-local
  settings. Once written, an id is never recomputed.
- Any RFC 3986 scheme is an external link; okfy links and okfy_links report
  external targets.
- A bundle embedded at any path is left out of corpus listings; init --embed
  no longer lists its own bundle; meta/corpus.md records manifest_digest.
- Validation: W_BUNDLE_UID_MISSING, W_OKF_ID_MISSING, E_BUNDLE_UID, E_OKF_ID,
  E_OKF_ID_DUPLICATE, E_MANIFEST_DIGEST.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An agent had one way to write: file a proposal and wait. That is right for
knowledge and wrong for session notes, observations and logs, where several
agents working at once need to see each other's writes now. meta/purpose.md
may now list `direct: {types: [...]}` under `write_policy: proposals`; a
concept of a listed type is written at once, everything else still goes to
proposals/. `write_policy` keeps its two literals and the pre-commit hook is
unchanged.

Concurrency is optimistic and adds no lock: the writer presents the sha256 it
read (the digest okfy_show already returns, or `absent` to create), and the
check and the write are one step under the existing review_lock. Of two
writers holding the same sha256 one lands and the other gets
E_WRITE_BASE_MOVED with the current sha256.

The lane relaxes review and nothing else. The injection and secret scans, the
`sources:` check and the archetype schema are the same functions a proposal
passes; a direct write cannot change `type` or anything an owner decision
wrote (verified, generated, the stale fields, supersedes/superseded_by); and
meta/ is never in the lane, so the policy cannot rewrite itself. The ledger
row is an `accept` with `origin: agent-direct` and the agent as actor, and it
does not dispose of an owner's standing rejection.

MCP gains okfy_write, which routes by the bundle's policy so the agent does
not need to know it: `channel: direct` or, through the okfy_propose path and
against the same sha256, `channel: proposal`. The CLI gains `okfy write`,
which refuses instead of falling back.

`okfy refine` is untouched and still neither scans, validates nor locks.
No version is bumped; CHANGELOG.md carries the entry as Unreleased. This
adds two test files to a tree that exports none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019a1Mcx6dNbaNqkq9dmYVgf
A review of the lane found no way to write what it must not, and five places
where two readers of one thing disagreed.

`okfy dismiss` and `unresolved_rejections` each folded the ledger for "does
a rejection still stand", and only one of them skipped an agent's direct
write: after one, the rejection stayed unresolved and the owner could no
longer dismiss it. Both now read `memory.latest_decisions`.

okfy_write tells the agent to send owner fields back as okfy_show returned
them, and its proposal fallback then refused a verified concept's whole text
for carrying `verified`. A `verified` equal to the base's own is dropped at
that intake, as the patch path already drops it; a different one is still
refused as forged.

A target deleted after it was read answered E_DIRECT_TARGET; it is a moved
base and now answers E_WRITE_BASE_MOVED with `current_sha256: absent`. An
extraction plan naming an unknown archetype escaped the MCP envelope as a
FileNotFoundError, and was loaded before routing; the schema is now loaded
only when a write reaches its check. An explicit `direct: null` read as an
absent block; it is E_DIRECT_KEY.

Each has a test that fails without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019a1Mcx6dNbaNqkq9dmYVgf
A bundle that lists direct.types still shipped an AGENTS.md whose memory
block says memory is never written to concepts directly. An agent that reads
files rather than tool descriptions was told to propose exactly the types the
owner had opened.

okfy package now appends one block, between okfy:direct markers, naming the
listed types and okfy write. It is rendered only for a well-formed direct
block under write_policy: proposals, so every other bundle's AGENTS.md is
byte-identical and the memory block keeps its 60-token bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019a1Mcx6dNbaNqkq9dmYVgf
scan_values collapsed every newline of a value before its second pass, so a
whole concept body became one line and the exfiltration co-occurrence rule,
written for one line, matched an outbound verb, the word password and a URL
anywhere in the document. On a 48-concept bundle of working notes 8 bodies
were refused, 5 of them for words in different paragraphs or different list
items, with the whole body as the excerpt.

A blank line and the start of a list item, heading, quote or table row are
now line boundaries. A phrase split by a hard wrap is still caught; one split
across a blank line or a list marker is seen by neither pass.

Separate from the direct-write lane on purpose: it changes what okfy propose
accepts too, and can be dropped from the branch on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019a1Mcx6dNbaNqkq9dmYVgf
…he lane

When the plan declares types, a concept of any other type is E_UNKNOWN_TYPE
the moment validate sees it. write_direct did not check that: the type missed
the lane, the MCP adapter filed a proposal invisible to search, and an owner
who accepted it got a bundle that no longer validates.

The same check, before the write: E_UNKNOWN_TYPE, nothing filed, the message
naming the declared types. Bundles that declare no types are unchanged.
_declared_types tolerates a caller with no archetype in hand.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019a1Mcx6dNbaNqkq9dmYVgf
vsov pushed a commit that referenced this pull request Oct 16, 2026
Version stays 0.29.0; everything is listed under "## Unreleased" in
CHANGELOG.md.

- Audit fixes A1-A10 since v0.29.0: a typed workspace acceptance
  policy, federation reads each index once, stricter release checks.
- #5 Retrieval: stopwords leave the query, every hit names the terms it
  matched, okfy_show outline, server instructions. The retrieval code
  digest moves, so recorded eval runs read stale.
- #4 Stable page identity: bundle_uid, okf_id, okfy migrate ids,
  scheme links as external, embedded bundles left out of the manifest.
- #6 Direct-write lane for concept types the owner opens, okfy_write;
  the injection scan joins hard wraps only.
- #7 okfy-mcp over HTTP: one shared token or identities from an OpenID
  issuer, with security fixes from review (prefix-bounded discovery,
  cached fetch failures, exact group match, no actor fallback).
- #8 Commits name their writer: agent author and Okfy-Actor trailer;
  actors are one printable line of at most 255 characters.

Contributed by @theaspirational in #4, #5, #6, #7 and #8.

Co-authored-by: Aspirational <dovganchin@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vsov

vsov commented Oct 16, 2026

Copy link
Copy Markdown
Owner

Thank you! This is integrated into the source tree and published in a0517dd ("Unreleased: audit fixes A1-A10 and contributed PRs #4-#8"). Your commits were cherry-picked with authorship kept; this repository is an export of the source, so the PR itself is closed rather than merged. Your tests live in the source tree, which the export does not ship.

Follow-ups: write_direct now keeps a page's okf_id on update and mints one on create, like review accept (it could drop or copy it before); okfy_write added to the stopped pilot arm's deny list; the injection-scan boundary is pinned by a test.

See CHANGELOG.md ## Unreleased for the full entry.

@vsov vsov closed this Oct 16, 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