Skip to content

sockets: a socket is a name bound to a program, apart from spaces - #144

Merged
losfair merged 9 commits into
mainfrom
claude/wonderful-shannon-hazzqi
Sep 9, 2026
Merged

sockets: a socket is a name bound to a program, apart from spaces#144
losfair merged 9 commits into
mainfrom
claude/wonderful-shannon-hazzqi

Conversation

@losfair

@losfair losfair commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Rationale

A socket was a file: the activated <space>/<path> was at once the program's location, the socket's name, and (because it sat under f:<space>/) its authorization boundary. One eBPF object serving two sockets meant two copies at two paths, each deployed separately.

This change separates the three. A program is an ordinary file in a space. A socket is a name in a namespace of this node's own, bound by a local activation to a program path, carrying its own --scope, --config, --max-streams, note, map and statistics. Many sockets may name one program.

synch socket activate git      --program code/bin/gateway.o --scope code --config upstream=git.internal
synch socket activate docs/git --program code/bin/gateway.o --scope docs
synch socket connect nas@cluster.example.com:git
synch socket ls nas@cluster.example.com:

The design is docs/SOCKET-PROGRAMS.md (status now implemented); docs/SOCKETS.md §2–§4, §9–§11 and docs/TREE-WRITES.md §2 are amended to describe the built thing.

What changes

  • Nothing socket-shaped enters the tree. The scanner no longer consults the activation table and never emits EntryKind::Socket; the variant stays so old records decode, and every reader treats it as a file with content. The verified Lean core is untouched.
  • Scope replaces authorization by position. A rooted member may open any socket; a delegate only one whose scope names a space it holds. Default is members only. New RefuseCode::OutOfScope, appended; SpaceNotDelegated stays for variant numbering, no longer emitted.
  • Wire frames change in place under sync/sock/1 (no version bump, per discussion): the first frame is SockRequest { Open, List }, SockOpen carries socket: String, SockOpened::Ok gains program_path, and List answers with the in-scope sockets so synch socket ls <origin>: gives the discovery synch ls used to. An un-upgraded peer fails at the frame decode, before any policy runs.
  • Deployment fan-out. A content change at a program path clears every dependent socket's map and logs once per socket, from the scanner and from API-source commits.
  • The tree-write refusal for activated paths is removed with no replacement. A program's sy_put_* write to a program path is a deployment like any other write. activate and ls -l instead show which activated program's manifest carries a tree-write grant covering the path.
  • Store migration v29 drops every existing activation and creates the new socket_activations (name PK, program_space, program_path, scope, config, max_streams, note, activated_at) empty. A name and a scope are grants; the migration does not infer them from a path. One warn line per dropped row names the synch socket activate <name> --program <space>/<path> that serves the program again. Every old <space>/<path> spelling is a legal name, so the old address can be restored by hand.
  • Control protocol v6: SocketActivate { target: name, program, scope }, name-addressed deactivate/ps/log, SocketLs.origin for the remote form. synch_socket_* MCP tools follow; a --space-confined MCP server is refused for name-addressed socket tools and scoped to the program's space for activate.
  • Bound: 256 activations per node replaces 64 per space. Sockets can now be activated over API-only sources.

Judgment calls worth a look: members-only default for scope; List includes in-scope sockets whose program is unpublished (empty root, printed unpublished); NotASocket is kept for a directory/symlink at a program path.

Commands run

cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test -p synch-core -p synch-store -p synch-net -p synch-sock
cargo test -p synch-engine --test sockets --test tree_writes
cargo test -p synch-cli

All green (store 305, net 65, sock 99 + 36 invoke + 17 examples, engine sockets 20 + tree_writes 5, cli 81 + 8 + 30 + 16). The engine stress suite and ignored synch-mpt tests were not run.

Platform and migration impact

  • Databases migrate automatically at open (v29). Every existing socket activation is dropped; the daemon log names each one with the command to re-activate it. Operators must re-activate their sockets, choosing a name and a scope.
  • Peers must upgrade before activating: an old caller's Open no longer decodes on a new callee and vice versa.
  • Control clients and daemons on different sides of v6 refuse each other.
  • apps/Hecatia had only registry strings and a comment updated; it was not built here (no Swift toolchain).

🤖 Generated with Claude Code

https://claude.ai/code/session_01RDXGRrMsUDV32dJwaV8xqc

One eBPF object at a program path may back many activated sockets, each
with its own config, cap and map. The socket path publishes a derived
Socket entry carrying the program's root, so peers, delegation,
replication and the Opened audit need no new rule; every write to the
program path deploys every socket it backs in one head, and program
paths join socket paths in the tree-write refusal.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDXGRrMsUDV32dJwaV8xqc
Rework the socket-program proposal: a socket is a name in its own
namespace bound by a local activation to a program file, with an
explicit scope saying which delegates may open it. Nothing new enters
the tree, so the derived-entry machinery of the first draft goes away;
the wire moves to sync/sock/2 to address sockets by name and gains a
List operation for discovery.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDXGRrMsUDV32dJwaV8xqc
…ite refusal

Change the Open and Opened frames in place rather than bumping the ALPN,
and treat a program-initiated write to a program path as the deployment
any other write to it is; activate lists the covering tree-write grant
beside the other deployment channels instead of refusing it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDXGRrMsUDV32dJwaV8xqc
TREE-WRITES.md §2 refused `sy_put_*` and writable-SFTP writes at an
activated path, at writer open and again inside the commit. That made one
write channel special among the several an activation already accepts — an
editor save, an adoption, an S3 `PUT` — when the operator is better served
by being shown the grant than by having it silently refused.

The refusal goes with no replacement, and so does the SFTP backend's
`entry_kind` refusal, which had nothing left to refuse: a socket entry is a
file with content, which is all it ever was on disk. `HostEntryKind::Socket`
had no producer left and goes with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDXGRrMsUDV32dJwaV8xqc
A socket was a file at `<space>/<path>`, and that one path was at once the
program's location, the socket's name, and — because it sat under `f:` — the
socket's authorization boundary. One object serving two sockets meant two
copies of the ELF, and `sy_socket_path`, which exists "so one object can back
several sockets", was reachable only by duplicating the thing it exists to
share.

Those three things come apart. A **program** is an ordinary file in this
node's tree, published, replicated and deployed as any file is. A **socket**
is a name in a namespace of its own, bound to a program by a local activation
that carries its own scope, config, stream cap, map and note; many sockets may
name one program. **Who may open it** is the activation's `scope`: rooted
members always, and the delegates of the spaces it names — empty, the default,
being members only.

Nothing socket-related enters the tree any more. The scanner stops consulting
the activation table and stops emitting `EntryKind::Socket`; the variant stays
so records older builds published still decode, and every reader treats it as
a file with content. Discovery moves to the protocol: `SockRequest` carries
`Open` or `List`, `SockOpen` addresses a socket by name, `Opened::Ok` carries
the program's path so a caller can still audit what answered, and a delegate
outside a socket's scope is told `OutOfScope`. The ALPN and the version are
unchanged: a peer on either side of the frame change fails at the handshake,
before any policy runs.

A content change at a path some activation names as its program is a
deployment to every socket that names it — one line per socket, and every
dependent map cleared — hooked into the scanner and into the API-source
commit. Schema v29 rewrites `socket_activations` around the name, mapping each
old row's `<space>/<path>` to a socket of that name, backed by the program at
that path, scoped to the space it sat in, so no delegate loses a socket it
could reach; each migrated row says which scope was written for it. The bound
becomes 256 activations per node, replacing the 64 per space that had no space
left to count in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDXGRrMsUDV32dJwaV8xqc
SOCKETS.md §2 and §3 are replaced by the socket namespace, the activation's
program, and the scope that says who may open it; §4 carries `SockRequest`,
`List`, `program_path` and `OutOfScope` under the unchanged ALPN; §9 is the
command surface with the grant `activate` prints; §10's bounds and failures
follow; §11 marks the version-identity amendment historical and describes the
table as it is. README, the control-plane skill, the examples README and
Hecatia's operation registry move to `<origin>:<name>`.

SOCKET-PROGRAMS.md is marked implemented and says where the built thing
settled a detail differently: `List` shows an in-scope socket whose program is
unpublished rather than hiding it, an entry that exists at a program path and
cannot be a program is `NotASocket` rather than `NoSuchPath`, and
`SpaceNotDelegated` is retired rather than merely unused.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDXGRrMsUDV32dJwaV8xqc
`is_program_path` was in the design for the tree-write gate, and the same
design removes that gate: a program's write to a program path is a deployment
like every other write to it. Nothing else asks the question — the deployment
fan-out and the dependents `activate` prints both want the activations, not a
boolean — so the predicate was public surface with no caller.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDXGRrMsUDV32dJwaV8xqc
A name and a scope are grants the operator makes, not ones a migration
can infer from a path. v29 now creates the new table empty, warns once
per dropped row with the activate command that serves the program again,
and the docs stop promising an invisible migration.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDXGRrMsUDV32dJwaV8xqc
@losfair
losfair merged commit 0b7698f into main Sep 9, 2026
24 of 25 checks passed
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