sigrank 1.0.45 → 1.0.47

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/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",