Repository navigation
Discover running TensorFold engines, including TensorFold Studio's - #145
Conversation
TensorFold Studio starts its own `tensorfold-native serve` on a new random loopback port every launch, so no registry entry can name it and IAK could not see it. The TensorFold status reader now also scans this user's processes for `tensorfold-native serve` / `tensorfold serve` (one `ps -U <uid> -o pid=,args=` on macOS, /proc on Linux with `comm` checked first, no sudo), takes the port from --port or, when there is none, from the listening socket via lsof, and the model and name from the command line. Engines are merged with the registry by port (the entry keeps its id and keyFile; an entry without an engine skips auto-detection) and published as source "discovered". GET-only rules unchanged. Keys come from files only: a registry keyFile, or the absolute --api-key-file a discovered engine was started with, read in the engine's own key-file format and refused when other users can read it. Process environments and --api-key values on a command line are never read or kept. TensorFold Studio makes a fresh key per launch, keeps it in memory and passes it as TENSORFOLD_API_KEY (app/electron/agent/engine/ supervisor.ts), so its engine reports metrics "auth_required", with its model taken from --name and labelled model_source "args". On by default when INTENT_TENSORFOLD_REGISTRY is set; INTENT_TENSORFOLD_DISCOVER=1 runs it without a registry, 0 (or `tensorfold-status --no-discover`) turns it off. New payload fields: name, app, model_source, memory.footprint_gb, top-level discovery (ok / off / unavailable). Tests: 15 new in test/tensorfold-discovery.test.mjs with ps, lsof and GET fixtures recorded from the three engines running on this Mac (Python 0.6.0, native 1.0.10, Studio), decoys, a fake /proc tree and a keyed engine.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 31c3b97b92
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const envRegistry = String(process.env.INTENT_TENSORFOLD_REGISTRY || '').trim(); | ||
| const registryRequired = typeof opts.registry === 'string' || (envRegistry !== '' && !['0', 'off', 'false', 'no'].includes(envRegistry.toLowerCase())); | ||
| const result = await runTensorFoldStatus({ | ||
| registryPath, registryRequired, discover: !opts['no-discover'], |
There was a problem hiding this comment.
Honor the discovery environment switch in the CLI
When INTENT_TENSORFOLD_DISCOVER=0 is set, invoking tensorfold-status without --no-discover still passes discover: true because this expression only inspects the CLI flag. This ignores the documented environment opt-out and causes the command to scan the process table and query discovered engines despite the operator disabling discovery.
Useful? React with 👍 / 👎.
| } else if (!available) { | ||
| port = DEFAULT_ENGINE_PORT; | ||
| portFrom = 'default'; |
There was a problem hiding this comment.
Preserve explicit port zero before applying the default
When an engine was explicitly started with --port 0 and lsof is unavailable or inaccessible, this branch substitutes port 8080. Port zero requests a dynamically assigned port, unlike an omitted --port, so the resulting card can probe or merge with an unrelated service on 8080 while missing the actual engine. Preserve whether zero was explicitly supplied and only use the documented default when the option was absent.
Useful? React with 👍 / 👎.
| /** One `ps -o args=` line (macOS) into an engine, or null when it is not `tensorfold serve`. */ | ||
| export function parseServeLine(pid, line) { | ||
| const text = String(line || ''); | ||
| const m = /(^|\/|-m )(tensorfold-native|tensorfold) serve(?= |$)/.exec(text); |
There was a problem hiding this comment.
Reject engine paths appearing as arbitrary command arguments
On macOS this regex accepts an engine-looking absolute path anywhere in the process line, not just as the executable or a recognized Python entry point. For example, /usr/bin/vim /tmp/tensorfold serve notes --port 9999 passes the subsequent prefix check because the prefix ends in /; discovery then treats the editor as a live engine and probes whatever unrelated service owns port 9999. Constrain matches to the command position or the supported Python launcher forms.
Useful? React with 👍 / 👎.
Review finding (@codexmb): parseServeLine took /tensorfold-native serve
anywhere in a joined ps line, so `printf /tmp/tensorfold-native serve ...
--host example.invalid --api-key-file /tmp/dummy-key` parsed as an engine;
its --host was kept as the connect address, local:true was forced, and the
key file the argv named was read and sent there as a Bearer token.
Now, fail closed:
- Identity comes from the kernel, not argv: the executable `lsof -d txt`
reports (macOS; `ps` comm is argv[0] there) or /proc/<pid>/exe (Linux)
must be tensorfold-native, or Python running `-m tensorfold serve` or an
absolute .../tensorfold entry script.
- Where to read comes from the pid's own LISTEN sockets (lsof, or
/proc/<pid>/fd + net/tcp{,6}), loopback or wildcard only; --host is not
parsed. A --port the pid does not listen on, several ports with no
--port, non-loopback-only sockets, or no lsof: not read, reason in
`skipped` (no default-port fallback any more).
- No key from a command line at all: --api-key-file is recorded only as
"was given", never read. A key comes only from a registry entry marked
engine tensorfold (as in #143), merged by port; the card says how.
- local is computed from the host again; collectTensorFoldStatus ignores a
non-loopback discovered address whatever the caller passes.
Tests: @codexmb's repro, an argv[0] that lies (/bin/sleep), a remote --host,
a --port the pid does not hold, end to end with a 0600 dummy key file and
a key-file spy: no read, no Authorization header, only loopback URLs.
Negative control: the previous parser accepts the repro line. 907/907.
Live on this Mac: Python 8091, Studio 54635, bench 8101 all verified.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What
Follow-up to #143. The TensorFold status reader (device heartbeat and
ide-agent-kit tensorfold-status) now also finds the TensorFold engines running on this machine and reads them with the same GET-only rules. They are merged with the registry by port and published assource: "discovered".Why
TensorFold Studio (the macOS app) starts its own engine,
.../TensorFold.app/Contents/Resources/engine/bin/tensorfold-native serve <model> --host 127.0.0.1 --port <random> --name <model>, with a new free port on every launch. A registry entry cannot name it, so IAK could not see it.How discovery works
ps -ww -U <uid> -o pid=,args=per read (about 50 ms measured). Linux:/proc/<pid>/cmdline, read only for processes whosecommistensorfold...orpython.... Matchestensorfold-native serveand the Pythontensorfold serve(alsopython -m tensorfold serve);grep, editors andtensorfold pullare not matched.--port(or--port=N). When there is none or it is0, from the pid's listening TCP socket vialsof. A process lsof can see that is not listening yet (still loading, or not an engine) gets no card until it listens. Only if lsof itself cannot run does the engine's default port 8080 stand in. Host from--host, with0.0.0.0/::mapped to loopback.--name, else the model argument as a repo id, else the repo a Hugging Face cache path belongs to.idandkeyFile. An entry with noengineneeds no auto-detection then. An entry with anotherenginestays skipped, and the reason says a TensorFold process holds the port. One card per port.INTENT_TENSORFOLD_REGISTRYis set.INTENT_TENSORFOLD_DISCOVER=1runs it with no registry at all;=0ortensorfold-status --no-discoverturns it off.Never read: process environments (
ps -E,/proc/<pid>/environ) and--api-keyvalues on a command line. The parser only records that a key was on argv and drops the value. The raw process table never leaves the module.Keys: what the app's source says
Read in TensorFold
origin/main(app/electron/agent/engine/supervisor.ts,service.ts,provider.ts,paths.ts) and checked on this Mac:randomBytes(32).toString('hex'), 64 hex chars) and passes it to the child only in the environment, asTENSORFOLD_API_KEY. The app's own tests assert the key never goes on argv, never appears in snapshots, events, logs or process bookkeeping, and changes on every model switch.supervisor.apiKey) and is attached per request to the built-in provider. The saved provider in~/.vontra/settings.jsonhasapiKey: ""(checked: empty).~/.vontra/engine/serve.jsonholds{pid, binary, owner}only (checked: those three keys, pid matches the running engine).~/.vontra/.secretkeyis the app's own settings and journal encryption key (app/electron/store/app-crypto.ts), not the engine key; it was not opened.~/Library/Application Support/TensorFold Studioholds only Chromium state.So no file holds the Studio engine's key. Its engine shows
metrics: "auth_required", the reason "TensorFold Studio keeps this engine's key in memory only, so its metrics stay locked", and the model from its--name(model_source: "args"). Reading the key from the engine's environment would work technically, but it would take a credential the app deliberately keeps private, so this PR does not. If we want Studio's numbers on the card, the clean fix is on the app side: start the engine with--metrics-open, or write a 0600 key file IAK can be pointed at.For engines started by hand with
--api-key-file <absolute path>, that file is read in the engine's own format (one key per line,label: keyor a bare key,#comments). The key is sent only on/metricsand/v1/slide/graphof that local engine and never logged or published. Like the engine, IAK refuses a key file other users can read. That rule now also applies to a registrykeyFileused for a TensorFold status read (stricter than #143, which only warned). A relative--api-key-fileis not read, because the engine's working directory is unknown.Payload change
Additive only;
schemastays 1. New top-leveldiscovery; new per-servername,app,model_source,memory.footprint_gb;sourcegains"discovered". Measured on this Mac (uik-daemon pointed at a local fake intent API; the block was 2768 bytes for three servers):For CodeWatch:
discovery:ok,off, orunavailablewhen the process scan failed. "Unavailable" is not "no engines running".app:"TensorFold Studio"for the app's bundled engine, so the card can say why its numbers are locked.model_source:"args"means "from its command line", not reported by the server.footprint_gb: the process's physical footprint on macOS (Metal buffers included). It is a separate field because the native MLX build reports noused, and footprint is never passed off asused.idis its--name, ortensorfold-<port>when it has none. A Studio relaunch therefore keeps the same id but gets a new port.Operator notes
After this merges, the live uik-daemon (already running with
INTENT_TENSORFOLD_REGISTRY) starts discovering on its next restart, no config change. On this Mac that means three cards: the 0.6.0 engine on 8091 (merged with themb-tensorfold-nemotronregistry entry), a nativebenchengine on 8101, and the Studio engine. SetINTENT_TENSORFOLD_DISCOVER=0if a bench engine should stay off the card.Tests
npm test: 904 pass, 0 fail (889 before, 15 new intest/tensorfold-discovery.test.mjs;test/tensorfold-status.test.mjsupdated for the new fields and made to keep discovery off). The full suite ran 3 times. Once,iak-mcp.mjs with empty tmux.allow OMITS tmux_runintest/mcp-server.test.mjsfailed on its 1.5 s boot window. It passed alone and in the other two full runs, and this PR does not touch that file.ps-macos-live.txt: the three realservelines from this Mac, home directory renamed.lsof-studio.txt: lsof output for the Studio engine's socket.health-studio-keyed.jsonandmetrics-studio-401.json, its live/healthand/metricsanswers.health-,metrics-andslide-graph-native-1.0.10.*, its live GET answers, with real traffic.ps-macos-decoys.txt: synthetic. Things that must not match, plus launcher shapes: spaces in paths,--port=,-m, a key on argv, no--port, a duplicate port./proctree. Asserts another user's processes are skipped, argv is read only for candidate processes, andenvironis never read.--api-key-filein label format; the key goes to/metricsand the graph only and never into the output.auth_requiredwith the model from args.unavailableand one warning; a broken registry still shows discovered engines.ps -E, readingenviron, keeping an argv key, sending the key to/health, skipping the mode check, keeping a non-listening process, no dedupe, scanning other users. No test reads this machine's process table or talks to a live engine.Measured live on this MacBook, GET only:
tensorfold-status --localagainst the real registry lists all three engines as above.🤖 Generated with Claude Code