-
Notifications
You must be signed in to change notification settings - Fork 0
feat(daemon): dual capability sockets + container topology (phase 6) #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # mykbd — v2 privileged write channel daemon (systemd unit, reference) | ||
| # | ||
| # Phase 6 of docs/v2-privileged-write-channel-DESIGN.md. The daemon is the | ||
| # ONLY process with write capability to the brain on disk; the Pi | ||
| # container mounts the brain read-only and reaches the daemon solely | ||
| # through the bind-mounted agent socket. | ||
| # | ||
| # Install: copy to /etc/systemd/system/mykbd.service, adjust the | ||
| # placeholders, then: systemctl daemon-reload && systemctl enable --now mykbd | ||
| # | ||
| # See docs/v2-container-topology.md for the full topology + the | ||
| # viloforge-platform integration seam. | ||
|
|
||
| [Unit] | ||
| Description=mykbd — mykb privileged write-channel daemon | ||
| After=network.target | ||
|
|
||
| [Service] | ||
| Type=simple | ||
|
|
||
| # Runs as the BRAIN-OWNING user. This uid owns ~/.mykb and the operator | ||
| # socket (0600) — it is the kernel-attested "operator" capability (§2.2). | ||
| User=%i | ||
|
|
||
| Environment=MYKB_DIR=%h/.mykb | ||
| # Dual-socket production mode (contract §2.2 amended): | ||
| # operator socket: host-local, 0600, this user only | ||
| # agent socket: bind-mounted into the Pi container, 0666, agent-capped | ||
| Environment=MYKB_OPERATOR_SOCKET=%h/.mykb/.mykbd-operator.sock | ||
| Environment=MYKB_AGENT_SOCKET=/run/mykbd/agent.sock | ||
|
|
||
| # /run/mykbd is the host-side directory bind-mounted into the container. | ||
| RuntimeDirectory=mykbd | ||
| RuntimeDirectoryMode=0755 | ||
|
|
||
| ExecStart=/usr/bin/node /opt/mykb/dist/daemon/main.js | ||
| Restart=on-failure | ||
| RestartSec=2 | ||
|
|
||
| # Hardening: the daemon needs write to ~/.mykb and /run/mykbd only. | ||
| NoNewPrivileges=true | ||
| ProtectSystem=strict | ||
| ProtectHome=read-only | ||
| ReadWritePaths=%h/.mykb /run/mykbd | ||
| PrivateTmp=true | ||
|
|
||
| [Install] | ||
| WantedBy=multi-user.target | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| # v2 Container Topology — Phase 6 | ||
|
|
||
| > Status: design + reference artifacts. Phase 6 of | ||
| > `v2-privileged-write-channel-DESIGN.md` (issue #1). | ||
| > | ||
| > Scope boundary: this document + `deploy/mykbd.service` + the | ||
| > dual-socket daemon code are the **mykb-repo** side. The actual | ||
| > `vf-agents-pi` container wiring (the argo/product manifests that add the | ||
| > RO brain mount + the agent-socket bind-mount) lives in | ||
| > **`viloforge-platform`** (per the standing "vafi config in | ||
| > viloforge-platform" project fact) and is a *deployment* action, not | ||
| > mykb-repo code. The integration seam is specified in §4 so that change | ||
| > is mechanical. | ||
|
|
||
| ## 1. The trust boundary, concretely | ||
|
|
||
| ``` | ||
| HOST (trusted) │ Pi CONTAINER (untrusted LLM) | ||
| │ | ||
| ~/.mykb/ ── owned by brain uid ────────┼── mounted READ-ONLY | ||
| │ │ bash > facts.jsonl → EROFS ✗ | ||
| │ (only writer) │ write tool → EROFS ✗ | ||
| ▼ │ python -c 'open(w)' → EROFS ✗ | ||
| mykbd (systemd, brain uid) │ MykbStore.append… ──┐ | ||
| ├─ operator socket 0600 ───────────┼── (NOT mounted in) │ | ||
| │ host operator / kb CLI only │ │ | ||
| └─ agent socket 0666 ───────────┼── bind-mounted in ◄───────┘ | ||
| capability capped at 'agent' │ the ONLY writable path | ||
| ``` | ||
|
Comment on lines
+17
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a language hint to the fenced block to satisfy markdownlint MD040. Suggested fix-```
+```text
HOST (trusted) │ Pi CONTAINER (untrusted LLM)
...
-```
+```🧰 Tools🪛 markdownlint-cli2 (0.22.1)[warning] 17-17: Fenced code blocks should have a language specified (MD040, fenced-code-language) 🤖 Prompt for AI Agents |
||
|
|
||
| The brain is bind-mounted **read-only** into the container, so every | ||
| direct syscall path to a brain file returns `EROFS` — closing the | ||
| `bash-bypass-known-gap` (the acceptance criterion, Phase 7). The only | ||
| success path from inside the container is "speak L4 to the daemon over | ||
| the bind-mounted **agent** socket," which is validated and capability- | ||
| capped. | ||
|
|
||
| ## 2. Capability without `SO_PEERCRED` (contract §2.2 amended) | ||
|
|
||
| Node has no public `SO_PEERCRED` API. Capability is therefore established | ||
| by **which socket** a connection arrives on, with the **kernel enforcing | ||
| who may `connect()`**: | ||
|
|
||
| | Socket | Mode | Reachable by | Capability | | ||
| |---|---|---|---| | ||
| | operator | `0600` | brain uid only (host operator, `kb` CLI) | `operator` — maintenance verbs, may assert `trust:'operator'` | | ||
| | agent | `0666` | anyone who can see the socket inode — i.e. only the container it is bind-mounted into | `agent` — mutation/read/workspace; asserted trust capped at `agent` | | ||
|
|
||
| `0666` on the agent socket is safe: **access is not privilege**. A | ||
| connection there can only ever obtain the `agent` capability; it cannot | ||
| call operator-only verbs nor forge `trust:'operator'` (dispatcher gate, | ||
| contract §5 / §3.1a). The socket is bind-mounted *only* into the trusted | ||
| Pi container, never exposed host-wide. | ||
|
|
||
| ## 3. Host setup (reference) | ||
|
|
||
| 1. Deploy the built daemon to `/opt/mykb` (`dist/`), Node ≥ the repo | ||
| engine. | ||
| 2. Install `deploy/mykbd.service` as `/etc/systemd/system/mykbd@.service` | ||
| (templated on the brain user), adjust `MYKB_DIR` / paths. | ||
| 3. `systemctl enable --now mykbd@<brainuser>`. | ||
| 4. The agent socket is created in `RuntimeDirectory=/run/mykbd` | ||
| (`/run/mykbd/agent.sock`); that directory is what the container mounts. | ||
|
|
||
| ## 4. viloforge-platform integration seam (the cross-repo change) | ||
|
|
||
| The Pi product manifest (`viloforge-platform/argo/products/vafi*`) needs | ||
| exactly two volume edits on the agents-pi pod/container: | ||
|
|
||
| 1. **Brain, read-only**: | ||
| `~/.mykb` (host) → container brain path, `readOnly: true`. | ||
| 2. **Agent socket, read-write** (a socket endpoint, not a brain file): | ||
| host `/run/mykbd/agent.sock` (or its dir) → the in-container path the | ||
| extension expects, and set `MYKB_SOCKET` (or `MYKB_STORE=rpc` + | ||
| `MYKB_SOCKET`) so `selectKnowledgeStore` (Phase 4/5) picks the RPC | ||
| store. No brain write mount. | ||
|
|
||
| Nothing else in the container changes: the extension and `kb` CLI already | ||
| auto-select the RPC store when the socket is present (Phases 4–5). When | ||
| that platform change lands, Phase 7's `bash-bypass-known-gap` flips | ||
| 🐛→✅ in `experiments/tool-gating/EXPERIMENT.md`. | ||
|
|
||
| ## 5. What is NOT in scope here | ||
|
|
||
| - The argo/manifest change itself (lands in viloforge-platform; §4 is the | ||
| spec for it). | ||
| - Host-mode `kb-pi` enforcement — explicitly out of v2 scope (parent | ||
| DESIGN §Scope); the host operator is trusted and uses the operator | ||
| socket / local store. | ||
| - Multi-host / cloud backends (parent DESIGN §Backend extensibility) — | ||
| the L2 Strategy seam exists; no second backend is built in v2. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,7 +35,14 @@ Decided: JSON-RPC 2.0 request/response objects, each frame length-prefixed (4-by | |
| - Length-prefix rather than newline-delimited: brain content (entry `text`, journal bodies, artifact content) contains arbitrary newlines; a newline delimiter would require escaping the payload. A byte count does not. | ||
| - The `{ op, args, id }` "Command" framing the parent DESIGN's pattern table promises maps onto JSON-RPC's `{ method, params, id }` 1:1. `method` = the L4 verb (§5); `params` = a single named-object (never positional — positional params couple the wire to argument order, an LSP hazard when backends evolve). | ||
|
|
||
| ### 2.2 Auth model → **OS peer credentials (`SO_PEERCRED`), single socket, capability derived per-connection** | ||
| ### 2.2 Auth model → **OS-enforced capability; dual capability sockets (amended Phase 6)** | ||
|
|
||
| > **AMENDED 2026-05-16 (Phase 6), per this doc's own change process** ("deviations require a revision to this doc, not an ad-hoc code decision", §1). **Forcing constraint:** Node.js exposes **no public `SO_PEERCRED` API** — the connecting peer's uid is not readable from a `net.Socket` without a native addon, which is against the project's deps-minimal ethos. **Resolution:** keep the *security property* (capability is kernel-attested, never client-asserted) but realize it through **two capability sockets** instead of one socket + `SO_PEERCRED`: | ||
| > | ||
| > - **operator socket** — created mode `0600`, owned by the brain uid. The kernel permits only the brain-owning uid to `connect()`. A connection here ⇒ `operator` capability. | ||
| > - **agent socket** — the one bind-mounted into the Pi container (Phase 6 topology). A connection here ⇒ `agent` capability; asserted `trust` capped at `agent` (§3.1a). | ||
| > | ||
| > This is **not an ad-hoc deviation**: the parent DESIGN §Operator-vs-extension explicitly sanctioned "operator commands gated by a different token / **socket** / capability", and §2.2's own text below already said splitting is protocol-compatible. The kernel still attests identity (filesystem-perms `connect()` enforcement instead of `SO_PEERCRED` readout); the daemon still never trusts a client-asserted capability. The capability resolver remains the injected Strategy seam (`DaemonOptions.resolveCapability`) decided in Phase 2 — the dual-socket resolver is one implementation of it; a future native-`SO_PEERCRED` single-socket resolver could replace it with no contract change. Single-socket dev-mode (default → `operator`) is retained for the host operator / tests (parent DESIGN §Dev-mode). The original single-socket+`SO_PEERCRED` text is kept below for design history. | ||
|
Comment on lines
+38
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unify the contract to one auth model to avoid contradictory normative guidance. This amendment is written as the active contract, but other normative sections still prescribe Also applies to: 301-301 🤖 Prompt for AI Agents |
||
|
|
||
| Decided: day-1 is OS perms, but *refined* beyond the parent DESIGN's "socket file mode + bind-mount" because envelope-v2's `trust` field (§3.1) forces the daemon to distinguish operator-capable connections from extension connections. | ||
|
|
||
|
|
@@ -291,7 +298,7 @@ The full testing pyramid (unit transport/validators → integration capability+h | |
| | Error taxonomy (§6) | ✅ `src/daemon/errors.ts` — full kind→code table; produced/asserted by the dispatch + scenario suites | | ||
| | `hello` handshake (§4.4) | ✅ implemented as a normal verb; capability echoed | | ||
| | Capability enforcement (§2.2) | ✅ per-verb gate + trust-cap, tested both capabilities incl. over the live socket | | ||
| | **SO_PEERCRED resolver** | ⏭ **deferred to Phase 6.** Node exposes no public SO_PEERCRED API; faking it in the scaffold would be dishonest. Resolution is an injected Strategy seam (`DaemonOptions.resolveCapability`, DIP); the kernel-peer-uid resolver lands with the systemd/container topology where it belongs. Default = `operator` (trusted dev-mode, parent DESIGN §Dev-mode). | | ||
| | ~~SO_PEERCRED resolver~~ → **dual capability sockets** | ✅ **Resolved Phase 6** (§2.2 amended). Node has no public SO_PEERCRED API, so capability is established by *which socket* a connection arrives on, kernel-enforced by `connect()` perms: operator socket `0600` (brain uid) vs agent socket `0666` (container, capability-capped). `DualSocketDaemon` (`src/daemon/dual-socket.ts`), 4 tests; single-socket dev default retained. Reference `deploy/mykbd.service` + `docs/v2-container-topology.md`. | | ||
| | L4 verbs → L3/core (§5) | ✅ full §5 surface wired except `area_stats` (needs MykbStore-internal db handle) and `rebuild` (CLI-inlined logic) → honestly UNSUPPORTED_OP via the CONTRACT_VERBS set, not faked | | ||
| | **L2 StorageBackend Strategy contract suite** | ⏭ **deferred.** Parent DESIGN §L2 is explicit: "v2 day-1: LocalFsBackend only." The scaffold re-homes `src/core/*` directly (sanctioned re-homing, not rewriting); the reusable StorageBackend contract suite is extracted when a second backend (S3/NFS) is actually built (contract §8). | | ||
| | Scenario e2e over real socket (capstone) | ✅ `tests/daemon/server.scenario.test.ts`, 4 scenario tests; representative verb of each group as operator + operator-only verb denied to agent over the wire + socket mode 0600 + split-write reassembly | | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,62 @@ | ||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * Dual capability sockets — `docs/v2-protocol-contract-DESIGN.md` §2.2 | ||||||||||||||||||||||||||
| * (amended Phase 6). | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * Node exposes no public `SO_PEERCRED` API, so capability is established | ||||||||||||||||||||||||||
| * by *which socket a connection arrives on*, with the kernel enforcing | ||||||||||||||||||||||||||
| * who may `connect()`: | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * - **operator socket** — mode `0600`, owned by the brain uid. Only the | ||||||||||||||||||||||||||
| * brain-owning uid (the host operator) can connect. ⇒ `operator`. | ||||||||||||||||||||||||||
| * - **agent socket** — mode `0666`, bind-mounted into the Pi container | ||||||||||||||||||||||||||
| * (a different uid). ⇒ `agent`; asserted `trust` capped at `agent`. | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * `0666` on the agent socket is safe by design: *access is not | ||||||||||||||||||||||||||
| * privilege*. A connection there only ever gets the `agent` capability — | ||||||||||||||||||||||||||
| * it cannot call operator-only verbs nor assert `trust:'operator'` (the | ||||||||||||||||||||||||||
| * dispatcher gate, §5/§3.1a). The socket is also only bind-mounted into | ||||||||||||||||||||||||||
| * the trusted container, never exposed host-wide. | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * Both sockets share ONE Dispatcher (one process, the sole writer), so | ||||||||||||||||||||||||||
| * the daemon's single event loop serializes JSONL appends across them. | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import { MykbDaemon } from './server.js'; | ||||||||||||||||||||||||||
| import { Dispatcher } from './dispatch.js'; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export interface DualSocketOptions { | ||||||||||||||||||||||||||
| brainPath: string; | ||||||||||||||||||||||||||
| operatorSocketPath: string; | ||||||||||||||||||||||||||
| agentSocketPath: string; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export class DualSocketDaemon { | ||||||||||||||||||||||||||
| private readonly operator: MykbDaemon; | ||||||||||||||||||||||||||
| private readonly agent: MykbDaemon; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| constructor(opts: DualSocketOptions) { | ||||||||||||||||||||||||||
| const dispatcher = new Dispatcher(opts.brainPath); | ||||||||||||||||||||||||||
| this.operator = new MykbDaemon({ | ||||||||||||||||||||||||||
| brainPath: opts.brainPath, | ||||||||||||||||||||||||||
| socketPath: opts.operatorSocketPath, | ||||||||||||||||||||||||||
| resolveCapability: () => 'operator', | ||||||||||||||||||||||||||
| socketMode: 0o600, | ||||||||||||||||||||||||||
| dispatcher, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| this.agent = new MykbDaemon({ | ||||||||||||||||||||||||||
| brainPath: opts.brainPath, | ||||||||||||||||||||||||||
| socketPath: opts.agentSocketPath, | ||||||||||||||||||||||||||
| resolveCapability: () => 'agent', | ||||||||||||||||||||||||||
| socketMode: 0o666, | ||||||||||||||||||||||||||
| dispatcher, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| async listen(): Promise<void> { | ||||||||||||||||||||||||||
| await Promise.all([this.operator.listen(), this.agent.listen()]); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+55
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make dual-socket startup atomic (rollback on partial listen failure). If one Suggested fix async listen(): Promise<void> {
- await Promise.all([this.operator.listen(), this.agent.listen()]);
+ await this.operator.listen();
+ try {
+ await this.agent.listen();
+ } catch (e) {
+ await this.operator.close().catch(() => {});
+ throw e;
+ }
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| async close(): Promise<void> { | ||||||||||||||||||||||||||
| await Promise.all([this.operator.close(), this.agent.close()]); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,16 +1,23 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * `mykbd` entrypoint — `docs/v2-protocol-contract-DESIGN.md` §4.1. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * `mykbd` entrypoint — `docs/v2-protocol-contract-DESIGN.md` §4.1 / §2.2. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Dev-mode launcher (parent DESIGN §Dev-mode strategy: `npm run | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * daemon:dev`). Production supervision (systemd) and the SO_PEERCRED | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * capability resolver are Phase 6 deliverables; this entrypoint runs the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * daemon against the operator's own brain with the default (operator) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * capability, which is exactly the trusted dev-mode contract. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Two modes: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - **dev / single-socket** (default, `npm run daemon:dev`): one socket, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * `operator` capability — the trusted host-operator loop (parent | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * DESIGN §Dev-mode). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - **production / dual-socket**: set `MYKB_OPERATOR_SOCKET` + | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * `MYKB_AGENT_SOCKET`. The operator socket (0600) is host-local; the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * agent socket (0666) is the one bind-mounted into the Pi container. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Capability is decided by which socket the connection arrives on | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * (§2.2 amended — no SO_PEERCRED needed). Run under systemd | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * (`deploy/mykbd.service`); see `docs/v2-container-topology.md`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import * as os from 'node:os'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import * as path from 'node:path'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { MykbDaemon } from './server.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { DualSocketDaemon } from './dual-socket.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function defaultBrainPath(): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return process.env.MYKB_DIR ?? path.join(os.homedir(), '.mykb'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -20,12 +27,33 @@ function defaultSocketPath(brainPath: string): string { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return process.env.MYKB_SOCKET ?? path.join(brainPath, '.mykbd.sock'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface Runnable { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| listen(): Promise<void>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| close(): Promise<void>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function main(): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const brainPath = defaultBrainPath(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const socketPath = defaultSocketPath(brainPath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const daemon = new MykbDaemon({ brainPath, socketPath }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const op = process.env.MYKB_OPERATOR_SOCKET; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ag = process.env.MYKB_AGENT_SOCKET; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let daemon: Runnable; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let banner: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (op && ag) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| daemon = new DualSocketDaemon({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| brainPath, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| operatorSocketPath: op, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| agentSocketPath: ag, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| banner = `mykbd (dual-socket) operator=${op} agent=${ag} (brain: ${brainPath})`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const socketPath = defaultSocketPath(brainPath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| daemon = new MykbDaemon({ brainPath, socketPath }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| banner = `mykbd (single-socket, operator) ${socketPath} (brain: ${brainPath})`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+37
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fail fast when only one dual-socket env var is set. Current logic silently drops to single-socket mode on partial config ( Suggested fix const op = process.env.MYKB_OPERATOR_SOCKET;
const ag = process.env.MYKB_AGENT_SOCKET;
+ if ((op && !ag) || (!op && ag)) {
+ throw new Error(
+ 'Set both MYKB_OPERATOR_SOCKET and MYKB_AGENT_SOCKET for dual-socket mode',
+ );
+ }
let daemon: Runnable;
let banner: string;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await daemon.listen(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`mykbd listening on ${socketPath} (brain: ${brainPath})`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(banner); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const shutdown = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| daemon | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -29,6 +29,13 @@ export interface DaemonOptions { | |||||||||||||||||||||||||||||||||
| socketPath: string; | ||||||||||||||||||||||||||||||||||
| /** Strategy seam for §2.2 capability resolution; default → 'operator'. */ | ||||||||||||||||||||||||||||||||||
| resolveCapability?: (socket: net.Socket) => Capability; | ||||||||||||||||||||||||||||||||||
| /** Socket file mode; default 0600 (operator-only connect, §2.2). */ | ||||||||||||||||||||||||||||||||||
| socketMode?: number; | ||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Share one Dispatcher across listeners (used by DualSocketDaemon so | ||||||||||||||||||||||||||||||||||
| * both capability sockets serve the same brain through one process). | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| dispatcher?: Dispatcher; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| export class MykbDaemon { | ||||||||||||||||||||||||||||||||||
|
|
@@ -38,7 +45,7 @@ export class MykbDaemon { | |||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| constructor(opts: DaemonOptions) { | ||||||||||||||||||||||||||||||||||
| this.opts = opts; | ||||||||||||||||||||||||||||||||||
| this.dispatcher = new Dispatcher(opts.brainPath); | ||||||||||||||||||||||||||||||||||
| this.dispatcher = opts.dispatcher ?? new Dispatcher(opts.brainPath); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| listen(): Promise<void> { | ||||||||||||||||||||||||||||||||||
|
|
@@ -51,9 +58,10 @@ export class MykbDaemon { | |||||||||||||||||||||||||||||||||
| return new Promise((resolve, reject) => { | ||||||||||||||||||||||||||||||||||
| server.once('error', reject); | ||||||||||||||||||||||||||||||||||
| server.listen(this.opts.socketPath, () => { | ||||||||||||||||||||||||||||||||||
| // 0600: only the brain-owning uid may even connect (defence in | ||||||||||||||||||||||||||||||||||
| // depth; capability still derives from SO_PEERCRED, not the mode). | ||||||||||||||||||||||||||||||||||
| fs.chmodSync(this.opts.socketPath, 0o600); | ||||||||||||||||||||||||||||||||||
| // Default 0600: only the brain-owning uid may connect (the | ||||||||||||||||||||||||||||||||||
| // operator-capability kernel gate, §2.2 amended). The agent | ||||||||||||||||||||||||||||||||||
| // socket overrides this to be container-connectable. | ||||||||||||||||||||||||||||||||||
| fs.chmodSync(this.opts.socketPath, this.opts.socketMode ?? 0o600); | ||||||||||||||||||||||||||||||||||
| server.off('error', reject); | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+61
to
65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle A permission/path error here can throw out of the Suggested fix server.listen(this.opts.socketPath, () => {
- // Default 0600: only the brain-owning uid may connect (the
- // operator-capability kernel gate, §2.2 amended). The agent
- // socket overrides this to be container-connectable.
- fs.chmodSync(this.opts.socketPath, this.opts.socketMode ?? 0o600);
+ // Default 0600: only the brain-owning uid may connect (the
+ // operator-capability kernel gate, §2.2 amended). The agent
+ // socket overrides this to be container-connectable.
+ try {
+ fs.chmodSync(this.opts.socketPath, this.opts.socketMode ?? 0o600);
+ } catch (e) {
+ server.off('error', reject);
+ server.close(() => reject(e));
+ return;
+ }
server.off('error', reject);
resolve();
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| resolve(); | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: vilosource/mykb
Length of output: 833
Update install instructions to use templated unit name, matching documented procedure.
The service file uses instance specifiers (
%ion line 23,%hon lines 25, 29, 44) that require a templated unit, but the install comments on lines 8–9 reference the non-template namemykbd.service. This conflicts with the documented correct procedure indocs/v2-container-topology.md(lines 59–61), which already instructs installing asmykbd@.serviceand enabling withmykbd@<brainuser>. Inconsistent install instructions risk incorrect deployment.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents