Skip to content

Discover running TensorFold engines, including TensorFold Studio's - #145

Merged
ThinkOffApp merged 2 commits into
mainfrom
tensorfold-discovery
Oct 14, 2026
Merged

ThinkOffApp merged 2 commits into
mainfrom
tensorfold-discovery

Conversation

@ThinkOffApp

Copy link
Copy Markdown
Owner

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 as source: "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

  • This user's processes only, no sudo. macOS: one ps -ww -U <uid> -o pid=,args= per read (about 50 ms measured). Linux: /proc/<pid>/cmdline, read only for processes whose comm is tensorfold... or python.... Matches tensorfold-native serve and the Python tensorfold serve (also python -m tensorfold serve); grep, editors and tensorfold pull are not matched.
  • Port from --port (or --port=N). When there is none or it is 0, from the pid's listening TCP socket via lsof. 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, with 0.0.0.0 / :: mapped to loopback.
  • Model and name from the command line: --name, else the model argument as a repo id, else the repo a Hugging Face cache path belongs to.
  • Merge by port. A loopback registry entry on the same port keeps its id and keyFile. An entry with no engine needs no auto-detection then. An entry with another engine stays skipped, and the reason says a TensorFold process holds the port. One card per port.
  • Cadence: same timer as before (the heartbeat interval, 60 s at most), so the scan reruns each read and a new engine or a relaunch on a new port shows up on its own.
  • Switches: on by default when INTENT_TENSORFOLD_REGISTRY is set. INTENT_TENSORFOLD_DISCOVER=1 runs it with no registry at all; =0 or tensorfold-status --no-discover turns it off.

Never read: process environments (ps -E, /proc/<pid>/environ) and --api-key values 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:

  • Studio makes a fresh random key on every engine launch (randomBytes(32).toString('hex'), 64 hex chars) and passes it to the child only in the environment, as TENSORFOLD_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.
  • The key lives only in the Electron main process's memory (supervisor.apiKey) and is attached per request to the built-in provider. The saved provider in ~/.vontra/settings.json has apiKey: "" (checked: empty).
  • ~/.vontra/engine/serve.json holds {pid, binary, owner} only (checked: those three keys, pid matches the running engine). ~/.vontra/.secretkey is 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 Studio holds 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: key or a bare key, # comments). The key is sent only on /metrics and /v1/slide/graph of 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 registry keyFile used for a TensorFold status read (stricter than #143, which only warned). A relative --api-key-file is not read, because the engine's working directory is unknown.

Payload change

Additive only; schema stays 1. New top-level discovery; new per-server name, app, model_source, memory.footprint_gb; source gains "discovered". Measured on this Mac (uik-daemon pointed at a local fake intent API; the block was 2768 bytes for three servers):

"tensorfold": {
  "schema": 1, "checked_at": "...", "scope": "local", "error": null,
  "discovery": "ok",
  "servers": [
    { "id": "mb-tensorfold-nemotron", "port": 8091, "source": "discovered", "name": null, "app": null,
      "model": "NVIDIA-Nemotron-3.5-Lightning-30B-A3B-MLX-4bit", "model_source": "health", "metrics": "ok", "...": "..." },
    { "id": "bench", "port": 8101, "source": "discovered", "name": "bench", "app": null,
      "model": "bench", "model_source": "health", "metrics": "ok",
      "memory": { "used_gb": null, "peak_gb": null, "budget_gb": null, "footprint_gb": 43.6 }, "...": "..." },
    { "id": "TensorFold/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-MLX-4bit", "host": "127.0.0.1", "port": 54635,
      "local": true, "source": "discovered", "name": "TensorFold/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-MLX-4bit",
      "app": "TensorFold Studio", "reachable": true,
      "reason": "metrics: auth required; TensorFold Studio keeps this engine's key in memory only, so its metrics stay locked",
      "model": "TensorFold/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-MLX-4bit", "model_source": "args",
      "metrics": "auth_required", "...": "every counter null" }
  ]
}

For CodeWatch:

  • discovery: ok, off, or unavailable when 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 no used, and footprint is never passed off as used.
  • Card ids: a discovered engine's id is its --name, or tensorfold-<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 the mb-tensorfold-nemotron registry entry), a native bench engine on 8101, and the Studio engine. Set INTENT_TENSORFOLD_DISCOVER=0 if a bench engine should stay off the card.

Tests

npm test: 904 pass, 0 fail (889 before, 15 new in test/tensorfold-discovery.test.mjs; test/tensorfold-status.test.mjs updated 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_run in test/mcp-server.test.mjs failed 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.

  • Fixtures:
    • ps-macos-live.txt: the three real serve lines from this Mac, home directory renamed.
    • lsof-studio.txt: lsof output for the Studio engine's socket.
    • Studio engine: health-studio-keyed.json and metrics-studio-401.json, its live /health and /metrics answers.
    • Native bench engine: health-, metrics- and slide-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.
  • Linux path: a fake /proc tree. Asserts another user's processes are skipped, argv is read only for candidate processes, and environ is never read.
  • Merge and dedupe: by port, with registry precedence.
  • Keyed engine: --api-key-file in label format; the key goes to /metrics and the graph only and never into the output.
  • Refusals: group-readable, relative and missing key files, key on argv.
  • Studio: auth_required with the model from args.
  • Monitor: discovery-only mode; scan failure gives unavailable and one warning; a broken registry still shows discovered engines.
  • Negative controls: eight deliberate bugs each turn a test red: ps -E, reading environ, 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 --local against the real registry lists all three engines as above.

🤖 Generated with Claude Code

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.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T17:02:29.659179Z 31c3b97 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread bin/cli.mjs
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'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +264 to +266
} else if (!available) {
port = DEFAULT_ENGINE_PORT;
portFrom = 'default';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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>
@ThinkOffApp
ThinkOffApp merged commit 56fd426 into main Oct 14, 2026
2 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.

1 participant