sockets: a socket is a name bound to a program, apart from spaces - #144
Merged
Conversation
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
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
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.
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 underf:<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.The design is
docs/SOCKET-PROGRAMS.md(status now implemented);docs/SOCKETS.md§2–§4, §9–§11 anddocs/TREE-WRITES.md§2 are amended to describe the built thing.What changes
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.scopenames a space it holds. Default is members only. NewRefuseCode::OutOfScope, appended;SpaceNotDelegatedstays for variant numbering, no longer emitted.sync/sock/1(no version bump, per discussion): the first frame isSockRequest { Open, List },SockOpencarriessocket: String,SockOpened::Okgainsprogram_path, andListanswers with the in-scope sockets sosynch socket ls <origin>:gives the discoverysynch lsused to. An un-upgraded peer fails at the frame decode, before any policy runs.sy_put_*write to a program path is a deployment like any other write.activateandls -linstead show which activated program's manifest carries a tree-write grant covering the path.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 thesynch 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.SocketActivate { target: name, program, scope }, name-addresseddeactivate/ps/log,SocketLs.originfor 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 foractivate.Judgment calls worth a look: members-only default for scope;
Listincludes in-scope sockets whose program is unpublished (empty root, printedunpublished);NotASocketis kept for a directory/symlink at a program path.Commands run
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-mpttests were not run.Platform and migration impact
Openno longer decodes on a new callee and vice versa.apps/Hecatiahad 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