sigrank 1.0.45 → 1.0.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.46
4
+
5
+ ### Security / privacy
6
+
7
+ * `enroll`: device label no longer defaults to the OS hostname; default is `sigrank <version> · <platform>` (opt-in override: `device_label` arg or `SIGRANK_DEVICE_LABEL`)
8
+ * `enroll` (MCP): refuses to re-bind an already-enrolled device; re-enroll from the CLI
9
+ * `submit_verified`: requires `confirm: true` to publish (not for `dry_run`); CLI/TUI pass it after their own consent
10
+ * `tokscale_developer_profile`: returns `workspace_count` only, no workspace/project names
11
+ * `validateApiBase`: https required except loopback hosts
12
+ * Pinned `ccusage` 20.0.30 and `tokscale` 4.18.0 exactly; removed stale `tokscale@latest` comment
13
+ * Proxy: rejects non-loopback Host headers and cross-site browser requests (403)
14
+ * Keystore: identity backups chmod 0600
15
+ * README / privacy-model corrected to list exactly what leaves the machine
16
+
3
17
  ## [1.0.44](https://github.com/SunrisesIllNeverSee/sigrank-mcp/releases/tag/v1.0.44) (2026-10-05)
4
18
 
5
19
  ### Features
package/README.md CHANGED
@@ -95,7 +95,7 @@ sigrank submit
95
95
  sigrank submit --dry-run
96
96
  ```
97
97
 
98
- That's it. sigrank reads your local AI session logs on-device, derives your token cascade (Υ Yield, Leverage, Velocity, 10xDEV), and publishes to [signalaf.com](https://signalaf.com). No paste, no transcript content — only the four token counts leave your machine.
98
+ That's it. sigrank reads your local AI session logs on-device, derives your token cascade (Υ Yield, Leverage, Velocity, 10xDEV), and publishes to [signalaf.com](https://signalaf.com). No paste, no transcript content — what leaves your machine is limited to the four token pillars plus the identifiers listed under [Privacy](#privacy).
99
99
 
100
100
  Or just explore without signing in:
101
101
 
@@ -314,7 +314,7 @@ Or if installed globally:
314
314
  | `self_improve` | `{text?}` | One-click optimize: diagnoses, suggests, and simulates the best change in a single call |
315
315
  | `tokscale_breakdown(threshold?)` | `{threshold?}` | Per-model token breakdown across platforms (models under threshold → "other") |
316
316
  | `tokscale_market_share()` | `{}` | AI tool market share: each tool's % of tokens/cost/messages, ranked. From local tokscale data |
317
- | `tokscale_developer_profile()` | `{}` | Per-developer usage profile across all detected tools: model mix, pillars, sessions, workspaces. Paths redacted |
317
+ | `tokscale_developer_profile()` | `{}` | Per-developer usage profile across all detected tools: model mix, pillars, sessions, workspace count (no names). Paths redacted |
318
318
  | `tokscale_model_trends()` | `{}` | Model adoption over time: per-model first/last seen, active days, month-by-month adoption curve |
319
319
  | `tokscale_cost_analysis()` | `{}` | Cost per developer per model: cost_per_million_tokens, cost_per_message, share_cost, client rollup |
320
320
  | `tokscale_device_profile()` | `{}` | Device fingerprinting: installed tools, session counts, active days, day-of-week distribution, concurrency. Paths redacted |
@@ -405,6 +405,8 @@ All adapters are token-only (no message content, no cost fields, no credentials)
405
405
 
406
406
  ## Privacy
407
407
 
408
+ - **Exactly what leaves your machine (signed submit):** the four token pillars (input, output, cache create, cache read) per window, your codename, device id, device public key, ed25519 signature, agent version, platform, message/session counts, and the weekly report block. `enroll` additionally sends a device label, which defaults to `sigrank <version> · <platform>` (your hostname is never sent unless you set `SIGRANK_DEVICE_LABEL` or pass `device_label`). Never sent: prompts, responses, file paths, repo/project names, or environment variables.
409
+ - **MCP safety rails.** The `enroll` tool refuses to re-bind a device that is already enrolled (re-enroll from the CLI), and `submit_verified` requires `confirm: true` to publish (not needed with `dry_run`). `tokscale_developer_profile` returns workspace counts only, never workspace or project names.
408
410
  - **Token-only persistence and submission.** Local-log adapters read usage metadata only. The optional proxy necessarily handles provider-bound request and response bytes in memory, but never persists their content; it writes only token counts, model/backend metadata, and timestamps. Only token telemetry is submitted to SigRank SignalAF.
409
411
  - **Local by default.** `tokenpull` reads only the selected or detected adapter paths on your device (e.g. `~/.claude/projects` for Claude, `~/.codex` for Codex, plus additional paths for Amp, Devin, Gemini, Goose, and other supported platforms). Numbers stay on your machine unless you explicitly submit with a codename.
410
412
  - **Background tooling excluded.** Memory plugins, observers, summarizers (e.g. `claude-mem`, `mem0`, `observer-sessions`) are filtered from both Claude and Codex reads. `subagents/` are kept — they represent real operator work.
@@ -417,7 +419,8 @@ All adapters are token-only (no message content, no cost fields, no credentials)
417
419
 
418
420
  | Var | Default | Description |
419
421
  | ----------------------- | ---------------------- | ---------------------------- |
420
- | `SIGRANK_API_BASE` | `https://signalaf.com` | Override the board host |
422
+ | `SIGRANK_API_BASE` | `https://signalaf.com` | Override the board host (https required; http only for loopback) |
423
+ | `SIGRANK_DEVICE_LABEL` | `sigrank <version> · <platform>` | Optional device label sent at enroll |
421
424
  | `SIGRANK_FETCH_TIMEOUT` | `10000` | Board API fetch timeout (ms) |
422
425
 
423
426
  ---
@@ -1099,6 +1099,8 @@ async function execCcusage(args) {
1099
1099
  if (existsSync(localEntrypoint)) {
1100
1100
  return execFileP(process.execPath, [localEntrypoint, ...args], options);
1101
1101
  }
1102
+ // Fallback only: the bundled (pinned) ccusage above is preferred; this resolves "ccusage"
1103
+ // from PATH when the bundled entrypoint is missing, so a PATH binary is trusted code here.
1102
1104
  return execFileP("ccusage", args, options);
1103
1105
  }
1104
1106
 
@@ -564,7 +564,7 @@ async function _freshTokscale(platform = "claude") {
564
564
  *
565
565
  * - ccusage : `ccusage <platform> daily --json` → 7d/30d/90d/all
566
566
  * - tokendash: refresh + read ~/.claude/token-dashboard.db → all (claude only)
567
- * - tokscale : `bunx tokscale@latest models --json` → all
567
+ * - tokscale : `tokscale models --json` (bundled, pinned) → all
568
568
  */
569
569
  export async function freshVerifierPillars(platform = "claude") {
570
570
  const p = platform || "claude";
@@ -61,7 +61,9 @@ export function persistIdentity(identity) {
61
61
  if (existsSync(PATH)) {
62
62
  try {
63
63
  const stamp = Date.now();
64
- copyFileSync(PATH, join(DIR, `${BACKUP_PREFIX}${stamp}`));
64
+ const bakPath = join(DIR, `${BACKUP_PREFIX}${stamp}`);
65
+ copyFileSync(PATH, bakPath);
66
+ chmodSync(bakPath, 0o600);
65
67
  pruneBackups();
66
68
  } catch {
67
69
  /* best-effort — don't block the write */
package/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://raw.githubusercontent.com/anthropics/mcpb/main/schemas/mcpb-manifest-v0.4.schema.json",
3
3
  "manifest_version": "0.4",
4
4
  "name": "sigrank",
5
- "version": "1.0.45",
5
+ "version": "1.0.46",
6
6
  "description": "Ranks AI coding operators by token cascade efficiency (Yield = cache_read \u00d7 output / input\u00b2). 25 tools including SigRank Standard record, yield calculation, local token usage pulls, signed submission, and tokscale-powered analytics (market share, developer profiles, model trends, cost analysis, device profiles, MCP usage, competitive intelligence). Privacy-first: runs locally, only four token counts leave the machine.",
7
7
  "author": {
8
8
  "name": "SunrisesIllNeverSee",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigrank",
3
- "version": "1.0.45",
3
+ "version": "1.0.46",
4
4
  "author": "SunrisesIllNeverSee (https://github.com/SunrisesIllNeverSee)",
5
5
  "description": "Upsilon measurement engine distributed through the sigrank CLI/MCP compatibility package, with SigRank public leaderboard tools.",
6
6
  "mcpName": "io.github.SunrisesIllNeverSee/sigrank-mcp",
@@ -82,8 +82,8 @@
82
82
  ],
83
83
  "dependencies": {
84
84
  "token-cascade": "0.2.1",
85
- "ccusage": "^20.0.19",
86
- "tokscale": "^4.7.0",
85
+ "ccusage": "20.0.30",
86
+ "tokscale": "4.18.0",
87
87
  "@modelcontextprotocol/server": "2.3.0"
88
88
  },
89
89
  "devDependencies": {
@@ -1719,10 +1719,14 @@ async function runEnroll({ label } = {}) {
1719
1719
 
1720
1720
  let out;
1721
1721
  try {
1722
- out = await callTool("enroll", {
1723
- code,
1724
- device_label: typeof label === "string" ? label : undefined,
1725
- });
1722
+ out = await callTool(
1723
+ "enroll",
1724
+ {
1725
+ code,
1726
+ device_label: typeof label === "string" ? label : undefined,
1727
+ },
1728
+ { allowReenroll: true }, // human at a TTY already consented above
1729
+ );
1726
1730
  } catch (e) {
1727
1731
  writeln(red(` ✗ ${e.message}`));
1728
1732
  process.exitCode = 1;
@@ -1778,6 +1782,7 @@ async function runSubmit({ platform = "claude", window } = {}) {
1778
1782
  platform,
1779
1783
  window: typeof window === "string" ? window : undefined,
1780
1784
  dry_run: process.argv.includes("--dry-run"),
1785
+ confirm: true, // explicit CLI invocation is the confirmation
1781
1786
  });
1782
1787
  } catch (e) {
1783
1788
  writeln(red(` ✗ ${e.message}`));
@@ -2904,7 +2904,7 @@ export async function runTui({
2904
2904
  didMulti = false;
2905
2905
  for (const p of platforms) {
2906
2906
  try {
2907
- const out = await callTool("submit_verified", { platform: p });
2907
+ const out = await callTool("submit_verified", { platform: p, confirm: true });
2908
2908
  const ws = out.windows || [];
2909
2909
  const received = ws.filter((w) => w.status === "received");
2910
2910
  const verified = received.filter(
@@ -2924,6 +2924,7 @@ export async function runTui({
2924
2924
  try {
2925
2925
  const out = await callTool("submit_verified", {
2926
2926
  platform: "multi",
2927
+ confirm: true,
2927
2928
  });
2928
2929
  const ws = out.windows || [];
2929
2930
  const received = ws.filter((w) => w.status === "received");
package/proxy.mjs CHANGED
@@ -311,6 +311,22 @@ export async function startProxy({
311
311
  };
312
312
 
313
313
  const server = http.createServer(async (req, res) => {
314
+ // DNS-rebinding / CSRF guard: only accept loopback Host headers on our own port, and
315
+ // refuse browser cross-site requests (Sec-Fetch-Site other than same-origin/none).
316
+ const hostHeader = String(req.headers.host || "");
317
+ const hm = /^(127\.0\.0\.1|localhost|\[::1\])(?::(\d+))?$/i.exec(hostHeader);
318
+ const boundPort = server.address()?.port;
319
+ const sfs = req.headers["sec-fetch-site"];
320
+ if (
321
+ !hm ||
322
+ (boundPort && Number(hm[2] || 80) !== boundPort) ||
323
+ (sfs !== undefined && sfs !== "same-origin" && sfs !== "none")
324
+ ) {
325
+ res.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
326
+ res.end("Forbidden\n");
327
+ req.resume();
328
+ return;
329
+ }
314
330
  let parsedUrl;
315
331
  try {
316
332
  parsedUrl = new URL(req.url || "/", `http://${req.headers.host || host}`);
@@ -2,13 +2,15 @@
2
2
 
3
3
  ## What leaves your machine
4
4
 
5
- **Every submission path sends only four numbers:**
5
+ **The usage content of every submission is four numbers:**
6
6
  - Input tokens
7
7
  - Output tokens
8
8
  - Cache create tokens
9
9
  - Cache read tokens
10
10
 
11
- That's the entire payload — no prompts, no code, no file contents, no conversation text. The MCP parses your input locally and sends only the four token counts to the server, which re-parses and re-scores them authoritatively.
11
+ The signed path additionally carries: codename, device id, device public key, ed25519 signature, agent version, platform, message/session counts, and the weekly report block. `enroll` sends a device label (default `sigrank <version> · <platform>`; your hostname is not sent unless you set `SIGRANK_DEVICE_LABEL` or pass `device_label` yourself).
12
+
13
+ Never sent: prompts, responses, code, file contents, file paths, repo or project names, or environment variables. The server re-parses and re-scores the pillars authoritatively.
12
14
 
13
15
  The optional `sigrank proxy` is a local Upsilon transport path to Anthropic/OpenAI, not a
14
16
  submission path to the SigRank leaderboard. When explicitly enabled, it necessarily receives and
@@ -32,8 +34,9 @@ only provider-reported token counts, model/backend metadata, and timestamps to
32
34
  - Your prompts or messages
33
35
  - Your code or file contents
34
36
  - Your tool calls or their results
35
- - Which AI platform you use (beyond token counts)
36
- - Your identity (only your chosen codename)
37
+ - Your hostname (unless you opt in via a device label)
38
+ - File paths, repo/project names, or environment variables
39
+ - Your identity (only your chosen codename; the platform name, agent version, and message/session counts are sent)
37
40
 
38
41
  The local proxy process is different from the Upsilon service: if you opt in, it
39
42
  handles your provider traffic in memory solely to forward it to Anthropic or
@@ -356,7 +356,6 @@ export async function tokscaleDeveloperProfile() {
356
356
  sessions_path_exists: c.sessions_path_exists ?? null,
357
357
  headless_supported: c.headless_supported ?? null,
358
358
  workspace_count: c.workspaces.size,
359
- workspaces: [...c.workspaces].sort(),
360
359
  models: c.models,
361
360
  };
362
361
  });
@@ -32,6 +32,8 @@ const ALLOWED_API_HOSTS = new Set([
32
32
  "sigeconomy.com",
33
33
  "localhost",
34
34
  "127.0.0.1",
35
+ "::1",
36
+ "[::1]",
35
37
  "test.local", // test fixtures only
36
38
  ]);
37
39
 
@@ -42,6 +44,10 @@ export function validateApiBase(urlStr) {
42
44
  if (!ALLOWED_API_HOSTS.has(u.hostname)) {
43
45
  throw new Error(`Blocked: API host '${u.hostname}' is not in the allowlist`);
44
46
  }
47
+ const loopback = ["localhost", "127.0.0.1", "::1", "[::1]", "test.local"].includes(u.hostname);
48
+ if (u.protocol !== "https:" && !(loopback && u.protocol === "http:")) {
49
+ throw new Error(`Blocked: API base must use https (http only for loopback): ${u.protocol}//${u.hostname}`);
50
+ }
45
51
  return urlStr;
46
52
  } catch (e) {
47
53
  if (e.message.startsWith("Blocked:")) throw e;
package/tools/enroll.mjs CHANGED
@@ -2,7 +2,6 @@
2
2
  * tools/enroll.mjs — enroll tool.
3
3
  */
4
4
 
5
- import os from "node:os";
6
5
  import { ensureIdentity, recordEnrollment } from "../identity/keystore.mjs";
7
6
  import { ENROLL_OUTPUT, ANNOTATIONS } from "./_schemas.mjs";
8
7
  import { TERMS_VERSION, PRIVACY_VERSION } from "../lib/constants.mjs";
@@ -28,7 +27,7 @@ export const TOOL_DEF = {
28
27
  device_label: {
29
28
  type: "string",
30
29
  description:
31
- "optional label for this device (default: hostname · agent version)",
30
+ "optional label for this device (default: sigrank <version> · <platform>; hostnames are never sent unless you pass a label yourself)",
32
31
  },
33
32
  },
34
33
  required: ["code"],
@@ -45,8 +44,23 @@ export async function handleEnroll(args, ctx) {
45
44
  "enroll requires a `code` — paste your connect code from signalaf.com → Settings → Connect a device.",
46
45
  );
47
46
  const id = ctx.opts.identity || ensureIdentity();
47
+ // Re-enroll guard: a model-invoked MCP call must never silently re-bind a device that
48
+ // already has an operator. The human CLI/TUI path sets opts.allowReenroll after its own
49
+ // explicit consent prompt.
50
+ if (id.codename && !ctx.opts.allowReenroll) {
51
+ return {
52
+ status: "already_enrolled",
53
+ codename: id.codename,
54
+ operator_id: id.operator_id ?? null,
55
+ device_id: id.device_id,
56
+ detail:
57
+ "This device is already enrolled. To re-enroll, run `npx sigrank enroll` in a terminal yourself; the MCP tool will not re-bind an enrolled device.",
58
+ };
59
+ }
48
60
  const deviceLabel = String(
49
- args?.device_label || `${os.hostname()} · ${id.agent_version}`,
61
+ args?.device_label ||
62
+ process.env.SIGRANK_DEVICE_LABEL ||
63
+ `sigrank ${id.agent_version} · ${process.platform}`,
50
64
  ).slice(0, 120);
51
65
  const res = await ctx.doFetch(`${ctx.apiBase}/api/v1/devices/enroll`, {
52
66
  method: "POST",
@@ -12,7 +12,7 @@ import { _computeReportBlock } from "./_report.mjs";
12
12
  export const TOOL_DEF = {
13
13
  name: "submit_verified",
14
14
  description:
15
- "Publish your LOCAL token runs to the SigRank board as a VERIFIED operator — the enrolled, signed path. Reads your pillars (tokenpull), builds the canonical Schema 1.0 snapshot per window, ed25519-signs it with your device key, and POSTs to /api/v1/snapshots. Requires `npx sigrank-mcp enroll` first (a bound device). Only signed submissions from a trusted device rank on the board. Token-only; the private key never leaves your machine. Pass dry_run:true to inspect the exact signed payload without publishing.",
15
+ "Publish your LOCAL token runs to the SigRank board as a VERIFIED operator — the enrolled, signed path. Reads your pillars (tokenpull), builds the canonical Schema 1.0 snapshot per window, ed25519-signs it with your device key, and POSTs to /api/v1/snapshots. Requires `npx sigrank-mcp enroll` first (a bound device). Only signed submissions from a trusted device rank on the board. Token-only; the private key never leaves your machine. Publishing requires `confirm: true` (ask the user first); pass dry_run:true to inspect the exact signed payload without publishing and without confirm.",
16
16
  annotations: {
17
17
  title: "Submit verified score",
18
18
  ...ANNOTATIONS.destructiveHint,
@@ -33,6 +33,11 @@ export const TOOL_DEF = {
33
33
  description:
34
34
  "source platform (default: claude). 'multi' = combined cascade summed across all locally-detected platforms (needs 2+ active); empty windows are skipped.",
35
35
  },
36
+ confirm: {
37
+ type: "boolean",
38
+ description:
39
+ "REQUIRED to publish: must be true, set only after the user has explicitly approved submitting. Not needed with dry_run:true.",
40
+ },
36
41
  dry_run: {
37
42
  type: "boolean",
38
43
  description:
@@ -52,6 +57,13 @@ export async function handleSubmitVerified(args, ctx) {
52
57
  detail: "Run `npx sigrank-mcp enroll` to bind this device first.",
53
58
  };
54
59
  }
60
+ if (!args?.dry_run && args?.confirm !== true) {
61
+ return {
62
+ status: "confirmation_required",
63
+ detail:
64
+ "submit_verified publishes to the public board. Ask the user for approval, then call again with confirm: true (or use dry_run: true to preview).",
65
+ };
66
+ }
55
67
  const platform = args?.platform || "claude";
56
68
 
57
69
  // MULTI: the combined cross-platform cascade. The dashboard already SUMS every
@@ -20,7 +20,7 @@ export const TOOL_DEF = {
20
20
  properties: {
21
21
  tools: {
22
22
  type: "array",
23
- description: "Per-tool profile with model mix, token pillars, session counts, workspaces",
23
+ description: "Per-tool profile with model mix, token pillars, session counts, workspace count",
24
24
  },
25
25
  summary: {
26
26
  type: "object",