Skip to content

fix(cli): keep item content off the private socket and say so - #2256

Merged
aicayzer merged 6 commits into
mainfrom
fix/099-socket-authority
Oct 21, 2026
Merged

aicayzer merged 6 commits into
mainfrom
fix/099-socket-authority

Conversation

@aicayzer

@aicayzer aicayzer commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Decision: the private control socket keeps out of item content, so items tag and items bulk-action being refused over --socket is correct. The bug was in the docs, which said the socket had the same authority as the owner, and in the CLI message, which did not say which commands need a key. This pull request writes that boundary into the contract, holds it with tests, corrects deploy/README.md and core/README.md, and makes the CLI's 401 message over --socket name which commands need a key and which work without one.

Evidence for the decision:

  • What the contract says. Private local authority is named on instance management doors (setup, recovery, keys, sign-ins, passkeys, owner, housekeeping, blob and connector management, restore) and, for items, only on name-only doors: search-and-filters/owner-summaries-authority (GET /items/summaries), items/owner-trash-actions and items/owner-trash-cli (trash through summaries), and items/job-read (job status). search-and-filters/owner-read-boundary keeps counts and export closed to it. No statement admitted it to an item content door, but none refused it explicitly either. This pull request adds that refusal as items/local-authority-content.
  • What the code does. The socket listener sets authority: local_process with no key (middleware/direct-authority.ts). Every content door decides with the key's type map (requireAuth, checkAuth, requireTypeAccess, requireWritableRow, computeTypeFilter), so it answers 401 when there is no key. Direct authority is admitted only through directAuthorityOnly and standingPermission, which no item content route uses.
  • How every item and edge route treats a socket request today (real marfa over a fresh local server's socket, plus curl --unix-socket). 401: items get/list/create/update/tag/untag/transition/delete/restore/purge/edges/backrefs/versions/stats, items bulk-action (all six actions, purge included), PUT/PATCH /items/{id}/metadata, every /items/{id}/extensions door, GET/POST /edges, GET/DELETE /edges/{id}, POST /edges/bulk, GET /search, GET /occurrences and export. Admitted: items summaries and every items trash command. items create and items update do not work over the socket either, so tag and bulk-action were never exceptions.
  • What the socket does reach on purpose. Blob bytes, uploads, URLs and locations (blobs/read-operator-key, and cli/instance.test.ts downloads a blob through the socket), and bulk-action job status and cancel (items/job-read). The server's own account can already read the data folder, so blob bytes add no reach. The words below say so.
  • The principle. Private local authority has no type map, and every content operation answers within one. The server account reaches content by minting a key through the socket (keys create), and that key's map limits its reach and its ID names it in the audit log. Bulk purge stays refused, consistent with the deliberate refusal of a private-socket purge elsewhere: the socket purges only one summary at a time, at its selected version, through the owner trash.

The server's behavior does not change. No route gains or loses access.

Changes

  • Contract: new items/local-authority-content in conformance/spec/items.md. When a request carries only private local authority to an item, edge, tag, metadata, extension, tombstone, search or occurrence operation, or to GET /events (other than item summaries, the owner trash, and reading or cancelling a bulk-action job), with a body the operation accepts, the server answers 401 unauthorized. events is in the statement because the stream carries item and edge content within a type map. Counts and exports are owned by search-and-filters/owner-read-boundary, which the statement's reason points to, so no two statements govern the same door.
  • Contract: new device/local-socket-names-key-needs in conformance/spec/device.md. On a 401 through --socket, the CLI says which commands need a key and which work through the socket without one.
  • Tests: new conformance/src/suites/compliance/local-authority-content.test.ts. Each door is tried over the socket, over HTTP with no key, and with the working key as a witness. Writes are checked to have changed nothing. It covers GET /metadata/tags, GET, PATCH and DELETE /edges/{id}, POST /edges/bulk (on its own pair of items), PUT /items/{id}/metadata, GET, PUT and DELETE /items/{id}/extensions/{namespace} (seeded first), POST /items/tombstones (a purged row's tombstone, checked unchanged) and GET /events. The bulk-action cases cover all six actions, and one real update_tags job checks that the socket queued nothing.
  • CLI: the 401 message over --socket keeps its first sentence and adds: "Item properties, edges, tags, search, counts, events and exports need a key. items summaries, items trash, the blobs commands, and items bulk-action job and items bulk-action cancel work through --socket without one". The --socket help text says the same, in the same words. COMMANDS.md is regenerated.
  • CLI tests: cli/instance.test.ts (real binary, real server socket) now covers items tag and all six items bulk-action subcommands, each with a key witness. device/contract.test.ts covers items tag and items bulk-action tags, and no longer lists blobs download or connectors deliveries body as 401 cases, because the real server admits both. The Rust unit test checks the new sentence.
  • Docs: deploy/README.md no longer says the socket has "the same machine authority" as the owner. It says what needs a key, what works without one, and points to keys create. core/README.md says the same and names items list, items tag, items bulk-action tags, events and export as refused.

Testing

  • Tests seen failing first. The five new compliance tests passed against unchanged code, because the code already refuses. To prove they can fail, I temporarily patched direct-authority.ts to give the socket a full key. All five failed (expected 200 to be 401 and similar), and I then reverted the patch. The CLI scenario, the device test and the Rust unit test each failed on the new sentence before the message changed, and passed after.
  • pnpm typecheck, pnpm lint, pnpm format:check: pass. cargo fmt, cargo clippy -p marfa-cli --all-targets -D warnings: pass. cargo test -p marfa-cli reference (regenerated COMMANDS.md): pass.
  • pnpm test:changed: 83 files, 1096 tests pass; 24 files, 238 tests pass.
  • Socket and authority conformance against a local marfa:up server: compliance/ local-authority-content, control-socket, owner-item-read, item-summaries, owner-trash, connector-direct-authority, owner, owner-sign-ins, version-writers and json-body-doors: 122 tests pass. cli/instance and cli/item-summaries: 21 tests pass. device/contract: 82 tests pass. spec-form and spec-citations: pass.
  • Real CLI over a fresh local server's socket. items tag, items bulk-action tags, items bulk-action purge and items create exit 5 with the new message. Over HTTP with no key, POST /items/{id}/tags and POST /items/bulk-actions answer 401. keys create through the socket mints a key, and that key then tags the item and queues a bulk tag job. The audit log names the mint local:process ("Local command") and names the tag and bulk job by the minted key. The refused socket requests left no audit rows.
  • Review fixes, run on the last commit. cargo fmt --check, cargo clippy -p marfa-cli --all-targets -- -D warnings and cargo test -p marfa-cli local:: pass. pnpm format:check, pnpm typecheck and pnpm lint pass. COMMANDS.md was regenerated with MARFA_WRITE_COMMANDS=1. spec-form, spec-citations and spec-statements pass (48 tests). The new compliance file passes (9 tests). Against a marfa:up server and the debug binary, cli/instance.test.ts and device/contract.test.ts pass (100 tests). A probe over a fresh server's socket confirmed blobs (stores, orphans, bytes, URL, locations) and bulk-action job read and cancel answer 200, and GET /events answers 401.
  • Not tested. The full conformance and CLI suites (CI runs them), and the packaged container's socket.

Notes for reviewers

Review depth: full, it changes what the private socket may do.

  • The server code does not change. This pull request pins existing behavior in the contract and fixes the docs and the CLI message that disagreed with it.
  • Known quirk, not changed here. A socket request with a malformed body to a content door gets 400 (body validation runs before the handler's key check), while HTTP with no credential gets 401 before the body is read. The new statement therefore covers requests "with a body the operation takes". This matches how a key without a grant is answered (400 before the permission check), and nothing can use the door either way.
  • Small overlaps with other open pull requests. fix(core): finish a push after a key change, keep waits at hand-over #2255 also touches core/README.md and feat(config): apply owner settings without restarting #2253 also touches core/marfa-cli/COMMANDS.md, in different lines. Whichever merges second rebases; COMMANDS.md is regenerated, not merged by hand.
  • For the release docs pass. The docs site's pages on the private socket and on marfa --socket should say what deploy/README.md now says: item properties, edges, tags, search, counts, events and exports need a key, which marfa --socket PATH keys create mints, while item summaries, the trash, blobs and bulk-action job status and cancel work without one. Also update any page that repeats "the same machine authority without an API key".

Checklist

  • The title is a scoped Conventional Commit.
  • New behavior has a test that was seen failing first, or no test was needed and the reason is above.
  • Docs, comments and the contract still say what is true after this change.

@aicayzer aicayzer left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: ready to merge

Summary. The private socket was never meant to reach item content: every content route already refuses it, so the fix is a contract statement that says so, a CLI message that names what needs a key, and READMEs that no longer promise more; the server is unchanged and the change is safe to merge.

First draft: 3/5. No blocking issue; 5 smaller ones found, fixed in c8c4a0785, 6e480662a and dc154ee9c.

Found.

  • The fixture did not try every door the statement names. Fixed: edges by id, edge bulk, metadata, extensions, tombstones and events now each have a socket refusal and a keyed witness.
  • The CLI message and READMEs said item content needs a key, but attachments and bulk-job status are reachable through the socket, on purpose (blobs/read-operator-key). Fixed: the words list what needs a key and what works without one.
  • The message named events without a statement behind it. Fixed: events are in the statement and its tests.
  • The statement overlapped search-and-filters/owner-read-boundary on counts and exports. Fixed: it points there instead.
  • Prose in deploy/README.md. Fixed.

Checked. Every item, edge, tag, metadata, extension, search, occurrence and event route answers 401 over the socket and over HTTP with no key; refused requests leave no audit rows; a key minted through the socket does the work and is named in the audit log.

@aicayzer
aicayzer marked this pull request as ready for review October 21, 2026 09:38
@aicayzer
aicayzer enabled auto-merge (squash) October 21, 2026 09:38
@aicayzer
aicayzer merged commit df4ba98 into main Oct 21, 2026
57 checks passed
@aicayzer
aicayzer deleted the fix/099-socket-authority branch October 21, 2026 10:16
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.

1 participant