@ixo/asks-mcp 1.6.0-dev.3 → 1.6.0-dev.4

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/README.md CHANGED
@@ -87,6 +87,8 @@ naming what is missing; nothing is posted and nothing is spent. `my_status` and
87
87
 
88
88
  On first run the server makes a key in `$OPENHANDS_HOME/<network>/key.json` (by default `~/.openhands/testnet/key.json`), readable only by you. The server refuses to use it if anyone else can read the file, anyone else can write to its directory, or either is a symlink. The mnemonic never leaves the server: no tool returns it and nothing logs it. The spend ledger (`spend.json`), the account it acts for (`principal.json`) and open-call drafts (`calls/`) sit beside it.
89
89
 
90
+ On Windows the server verifies the actual ACL using Windows PowerShell, because Node's POSIX mode bits cannot express Windows permissions. The current Windows user must have Full control; only that user, SYSTEM and the built-in Administrators group may have access. Inherited private permissions are accepted. The server does not rewrite Windows ACLs, and refuses to read a key when it cannot verify them. For a refusal, check the directory and file under **Properties > Security**, or choose an `OPENHANDS_HOME` that already has private permissions. No permission-check bypass is available.
91
+
90
92
  Spending tools preview first and need the exact total confirmed; daily caps are counted on disk, so restarting the client does not reset them. Earnings go only to `OPENHANDS_HOME_ADDRESS`.
91
93
 
92
94
  ## The hosted server instead
package/dist/asks-mcp.mjs CHANGED
@@ -8058,7 +8058,14 @@ function reportProblems(raw, requirement, fileIds = []) {
8058
8058
  }
8059
8059
  function readReport(envelope) {
8060
8060
  const doc = obj(envelope);
8061
- const report = obj(doc?.report) ?? (doc?.kind === REPORT_KIND ? doc : null);
8061
+ let report = obj(doc?.report) ?? (doc?.kind === REPORT_KIND ? doc : null);
8062
+ if (!report && typeof doc?.deliverable === "string" && doc.deliverable.length <= REPORT_LIMITS.bytes) {
8063
+ try {
8064
+ const pasted = obj(JSON.parse(doc.deliverable));
8065
+ if (pasted?.kind === REPORT_KIND && obj(pasted.summary)) report = pasted;
8066
+ } catch {
8067
+ }
8068
+ }
8062
8069
  if (!report) return null;
8063
8070
  const summary = obj(report.summary) ?? {};
8064
8071
  const read9 = {
@@ -8090,6 +8097,16 @@ function readReport(envelope) {
8090
8097
  sections: Object.entries(obj(report.sections) ?? {}).map(([title, value]) => ({ title, text: String(value) }))
8091
8098
  };
8092
8099
  const files = (Array.isArray(doc?.files) ? doc.files : []).map(obj).filter((one) => one !== null && typeof one.id === "string" && typeof one.url === "string" && STORED.test(one.url)).slice(0, 24).map((one) => ({ id: String(one.id), url: String(one.url), sha256: String(one.sha256 ?? ""), mediaType: String(one.mediaType ?? "application/octet-stream"), name: String(one.name ?? one.id) }));
8100
+ for (const field2 of Object.values(doc ?? {})) {
8101
+ if (!Array.isArray(field2)) continue;
8102
+ for (const value of field2) {
8103
+ const file3 = obj(value);
8104
+ if (!file3 || !isStoredFile(file3.content) || files.some((one) => one.url === file3.content) || files.length >= 24) continue;
8105
+ let id = `attachment-${files.length + 1}`;
8106
+ while (files.some((one) => one.id === id)) id += "-file";
8107
+ files.push({ id, url: file3.content, sha256: "", mediaType: str(file3.type) ?? "application/octet-stream", name: str(file3.name) ?? "Attachment" });
8108
+ }
8109
+ }
8093
8110
  return { doc: read9, files };
8094
8111
  }
8095
8112
  function formAnswersOf(envelope) {
@@ -30395,6 +30412,73 @@ var init_site = __esm({
30395
30412
  }
30396
30413
  });
30397
30414
 
30415
+ // src/keyPermissions.ts
30416
+ import { spawnSync } from "node:child_process";
30417
+ import { lstatSync } from "node:fs";
30418
+ import { win32 } from "node:path";
30419
+ function assertWindowsPrivatePath(path, run2 = spawnSync) {
30420
+ const root = process.env.SystemRoot;
30421
+ if (!root || !win32.isAbsolute(root)) throw new Error(`Cannot verify Windows permissions for ${path}: SystemRoot is unavailable. No key was read.`);
30422
+ const powershellHome = win32.join(root, "System32", "WindowsPowerShell", "v1.0");
30423
+ const env = { ...process.env };
30424
+ for (const name8 of Object.keys(env)) if (/^(psmodulepath|openhands_acl_path)$/i.test(name8)) delete env[name8];
30425
+ const result = run2(win32.join(powershellHome, "powershell.exe"), [
30426
+ "-NoLogo",
30427
+ "-NoProfile",
30428
+ "-NonInteractive",
30429
+ "-EncodedCommand",
30430
+ Buffer.from(CHECK_ACL, "utf16le").toString("base64")
30431
+ ], {
30432
+ env: { ...env, PSModulePath: win32.join(powershellHome, "Modules"), OPENHANDS_ACL_PATH: path },
30433
+ timeout: 3e4,
30434
+ windowsHide: true,
30435
+ stdio: "ignore"
30436
+ });
30437
+ if (result.error || result.signal || result.status !== 0) {
30438
+ const why = result.error?.code === "ETIMEDOUT" ? "inspection timed out" : `inspection exit ${result.status ?? "unavailable"}`;
30439
+ throw new Error(`Cannot verify private Windows permissions for ${path} (${why}). Only your Windows account, SYSTEM and Administrators may have access, and your account must have Full control. Check Properties > Security, or choose a private OPENHANDS_HOME. No key was read.`);
30440
+ }
30441
+ }
30442
+ function assertSafe(path, what, platform = process.platform, run2) {
30443
+ const stat = lstatSync(path);
30444
+ if (stat.isSymbolicLink()) throw new Error(`${path} is a symlink. A key that lives where another program decides is not a key we will use.`);
30445
+ if (what === "file" ? !stat.isFile() : !stat.isDirectory()) throw new Error(`${path} is not a ${what}.`);
30446
+ if (platform === "win32") {
30447
+ assertWindowsPrivatePath(path, run2);
30448
+ return;
30449
+ }
30450
+ const mode = stat.mode & 511;
30451
+ const tooOpen = what === "file" ? mode & 63 : mode & 18;
30452
+ if (tooOpen) throw new Error(`${path} is readable or writable by others (mode ${mode.toString(8)}). Run: chmod ${what === "file" ? "600" : "700"} ${path}`);
30453
+ }
30454
+ var CHECK_ACL;
30455
+ var init_keyPermissions = __esm({
30456
+ "src/keyPermissions.ts"() {
30457
+ "use strict";
30458
+ CHECK_ACL = String.raw`
30459
+ $ErrorActionPreference = 'Stop'
30460
+ try {
30461
+ $item = Get-Item -LiteralPath $env:OPENHANDS_ACL_PATH -Force
30462
+ if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { exit 2 }
30463
+ $acl = Get-Acl -LiteralPath $env:OPENHANDS_ACL_PATH
30464
+ $me = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
30465
+ $trusted = @($me, 'S-1-5-18', 'S-1-5-32-544')
30466
+ if ($trusted -notcontains $acl.GetOwner([Security.Principal.SecurityIdentifier]).Value) { exit 3 }
30467
+ $mine = $false
30468
+ foreach ($rule in $acl.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier])) {
30469
+ if (($rule.PropagationFlags -band [Security.AccessControl.PropagationFlags]::InheritOnly) -ne 0) { continue }
30470
+ if ($rule.AccessControlType -ne [Security.AccessControl.AccessControlType]::Allow) { continue }
30471
+ if ($trusted -notcontains $rule.IdentityReference.Value) { exit 3 }
30472
+ if ($rule.IdentityReference.Value -eq $me -and
30473
+ ($rule.FileSystemRights -band [Security.AccessControl.FileSystemRights]::FullControl) -eq [Security.AccessControl.FileSystemRights]::FullControl) { $mine = $true }
30474
+ }
30475
+ if (-not $mine) { exit 3 }
30476
+ exit 0
30477
+ } catch { exit 4 }
30478
+ `;
30479
+ }
30480
+ });
30481
+
30398
30482
  // ../../node_modules/.pnpm/cosmjs-types@0.11.0/node_modules/cosmjs-types/utf8.js
30399
30483
  var require_utf82 = __commonJS({
30400
30484
  "../../node_modules/.pnpm/cosmjs-types@0.11.0/node_modules/cosmjs-types/utf8.js"(exports) {
@@ -49621,26 +49705,14 @@ __export(keys_exports, {
49621
49705
  signer: () => signer,
49622
49706
  writePin: () => writePin
49623
49707
  });
49624
- import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
49708
+ import { chmodSync, existsSync, lstatSync as lstatSync2, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
49625
49709
  import { homedir } from "node:os";
49626
49710
  import { join as join3 } from "node:path";
49627
- function assertSafe(path, what) {
49628
- const stat = lstatSync(path);
49629
- if (stat.isSymbolicLink()) {
49630
- throw new Error(`${path} is a symlink. A key that lives where another program decides is not a key we will use.`);
49631
- }
49632
- const mode = stat.mode & 511;
49633
- const tooOpen = what === "file" ? mode & 63 : mode & 18;
49634
- if (tooOpen) {
49635
- throw new Error(
49636
- `${path} is readable or writable by others (mode ${mode.toString(8)}). Run: chmod ${what === "file" ? "600" : "700"} ${path}`
49637
- );
49638
- }
49639
- }
49640
49711
  function ensureDir() {
49641
49712
  const dir = DIR();
49642
49713
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 448 });
49643
- chmodSync(dir, 448);
49714
+ if (lstatSync2(dir).isSymbolicLink()) throw new Error(`${dir} is a symlink.`);
49715
+ if (process.platform !== "win32") chmodSync(dir, 448);
49644
49716
  assertSafe(dir, "dir");
49645
49717
  }
49646
49718
  async function loadKey() {
@@ -49667,7 +49739,7 @@ async function loadKey() {
49667
49739
  const [account] = await wallet.getAccounts();
49668
49740
  const made = { mnemonic: wallet.mnemonic, address: account.address };
49669
49741
  writeFileSync(KEY_FILE2, JSON.stringify(made, null, 2), { mode: 384 });
49670
- chmodSync(KEY_FILE2, 384);
49742
+ if (process.platform !== "win32") chmodSync(KEY_FILE2, 384);
49671
49743
  assertSafe(KEY_FILE2, "file");
49672
49744
  cached2.set(KEY_FILE2, made);
49673
49745
  return made;
@@ -49691,6 +49763,10 @@ async function signer() {
49691
49763
  function readPin() {
49692
49764
  const PIN_FILE = pinFile();
49693
49765
  if (!existsSync(PIN_FILE)) return null;
49766
+ const { ino, mtimeMs, ctimeMs } = lstatSync2(PIN_FILE);
49767
+ const stamp = `${ino}:${mtimeMs}:${ctimeMs}`;
49768
+ const seen2 = pins.get(PIN_FILE);
49769
+ if (seen2?.stamp === stamp) return seen2.pin;
49694
49770
  assertSafe(PIN_FILE, "file");
49695
49771
  let parsed;
49696
49772
  try {
@@ -49701,7 +49777,9 @@ function readPin() {
49701
49777
  if (typeof parsed.principal !== "string" || !PRINCIPAL.test(parsed.principal) || typeof parsed.agent !== "string") {
49702
49778
  throw new Error(`${PIN_FILE} does not name an account. Remove it and call connect_account again.`);
49703
49779
  }
49704
- return { principal: parsed.principal, agent: parsed.agent, pinnedAt: typeof parsed.pinnedAt === "string" ? parsed.pinnedAt : "", ...parsed.signed === true ? { signed: true } : {} };
49780
+ const pin = { principal: parsed.principal, agent: parsed.agent, pinnedAt: typeof parsed.pinnedAt === "string" ? parsed.pinnedAt : "", ...parsed.signed === true ? { signed: true } : {} };
49781
+ pins.set(PIN_FILE, { stamp, pin });
49782
+ return pin;
49705
49783
  }
49706
49784
  function writePin(pin) {
49707
49785
  if (!PRINCIPAL.test(pin.principal)) throw new Error("that is not an ixo account address");
@@ -49709,17 +49787,18 @@ function writePin(pin) {
49709
49787
  const PIN_FILE = pinFile();
49710
49788
  const temporary = `${PIN_FILE}.tmp`;
49711
49789
  writeFileSync(temporary, JSON.stringify(pin, null, 2), { mode: 384 });
49712
- chmodSync(temporary, 384);
49790
+ if (process.platform !== "win32") chmodSync(temporary, 384);
49713
49791
  renameSync(temporary, PIN_FILE);
49714
49792
  assertSafe(PIN_FILE, "file");
49715
49793
  }
49716
49794
  function clearPin() {
49717
49795
  rmSync(pinFile(), { force: true });
49718
49796
  }
49719
- var DIR, KEY_FILE, cached2, keyLocation, pinFile, PRINCIPAL, pinLocation;
49797
+ var DIR, KEY_FILE, cached2, keyLocation, pinFile, PRINCIPAL, pins, pinLocation;
49720
49798
  var init_keys = __esm({
49721
49799
  "src/keys.ts"() {
49722
49800
  "use strict";
49801
+ init_keyPermissions();
49723
49802
  init_net();
49724
49803
  DIR = () => join3(process.env.OPENHANDS_HOME || join3(homedir(), ".openhands"), currentNetwork2());
49725
49804
  KEY_FILE = () => join3(DIR(), "key.json");
@@ -49727,6 +49806,7 @@ var init_keys = __esm({
49727
49806
  keyLocation = () => KEY_FILE();
49728
49807
  pinFile = () => join3(DIR(), "principal.json");
49729
49808
  PRINCIPAL = /^(ixo1[a-z0-9]{38,58}|self)$/;
49809
+ pins = /* @__PURE__ */ new Map();
49730
49810
  pinLocation = () => pinFile();
49731
49811
  }
49732
49812
  });
@@ -259464,8 +259544,8 @@ function noteIgnored(agent, pinned, others, log) {
259464
259544
  for (const one of fresh2) told.add(`${agent}:${pinned}:${one}`);
259465
259545
  log(`asks-mcp: ignoring ${fresh2.length === 1 ? "a connection" : "connections"} to this key from ${fresh2.join(", ")}: it is pinned to ${pinned === SELF ? "itself" : pinned}, and only connect_account changes that.`);
259466
259546
  }
259467
- async function actingFor(agent, reads2 = chainReads, pins = keyPins) {
259468
- const pinned = pinOf(agent, pins);
259547
+ async function actingFor(agent, reads2 = chainReads, pins2 = keyPins) {
259548
+ const pinned = pinOf(agent, pins2);
259469
259549
  if (!pinned || pinned === SELF) return null;
259470
259550
  let found;
259471
259551
  try {
@@ -259475,14 +259555,14 @@ async function actingFor(agent, reads2 = chainReads, pins = keyPins) {
259475
259555
  }
259476
259556
  return found ? actingOf(found) : null;
259477
259557
  }
259478
- async function actingOrWhy(agent, reads2 = chainReads, pins = keyPins, log = stderr) {
259558
+ async function actingOrWhy(agent, reads2 = chainReads, pins2 = keyPins, log = stderr) {
259479
259559
  let acting = null;
259480
259560
  try {
259481
- acting = await actingFor(agent, reads2, pins);
259561
+ acting = await actingFor(agent, reads2, pins2);
259482
259562
  } catch (error54) {
259483
259563
  return { acting: null, problem: error54 instanceof Error ? error54.message : String(error54), candidates: [] };
259484
259564
  }
259485
- const pinned = pinOf(agent, pins);
259565
+ const pinned = pinOf(agent, pins2);
259486
259566
  let standing;
259487
259567
  try {
259488
259568
  standing = (await reads2.connectionsTo(agent)).map((one) => one.principal);
@@ -259499,16 +259579,16 @@ function candidateWords(candidates) {
259499
259579
  if (!candidates.length) return null;
259500
259580
  return `Not acting for anyone: ${candidates.length === 1 ? "an account has" : `${candidates.length} accounts have`} connected this key (${list5(candidates)}), and none has been chosen. Anybody can grant to a public address, so ask the person which is theirs and call connect_account with principal set to it (or principal: "self" to stay as this key and stop being asked).`;
259501
259581
  }
259502
- async function connectAs(agent, choice, reads2 = chainReads, pins = keyPins, now3 = /* @__PURE__ */ new Date(), log = stderr) {
259503
- const pinned = pinOf(agent, pins);
259582
+ async function connectAs(agent, choice, reads2 = chainReads, pins2 = keyPins, now3 = /* @__PURE__ */ new Date(), log = stderr) {
259583
+ const pinned = pinOf(agent, pins2);
259504
259584
  if (choice === SELF) {
259505
- if (pinned !== SELF) pins.write({ principal: SELF, agent, pinnedAt: now3.toISOString() });
259585
+ if (pinned !== SELF) pins2.write({ principal: SELF, agent, pinnedAt: now3.toISOString() });
259506
259586
  return { kind: "self", pinnedNow: pinned !== SELF };
259507
259587
  }
259508
259588
  if (choice) {
259509
259589
  const found = await reads2.connectionFor(agent, choice);
259510
259590
  if (!found) return { kind: "not-granted", principal: choice };
259511
- if (pinned !== choice) pins.write({ principal: choice, agent, pinnedAt: now3.toISOString() });
259591
+ if (pinned !== choice) pins2.write({ principal: choice, agent, pinnedAt: now3.toISOString() });
259512
259592
  const others = (await reads2.connectionsTo(agent)).map((one) => one.principal).filter((one) => one !== choice);
259513
259593
  return { kind: "connected", acting: actingOf(found), pinnedNow: pinned !== choice, others };
259514
259594
  }
@@ -259522,13 +259602,13 @@ async function connectAs(agent, choice, reads2 = chainReads, pins = keyPins, now
259522
259602
  const standing = (await reads2.connectionsTo(agent)).map((one) => one.principal);
259523
259603
  return standing.length ? { kind: "choose", principals: standing } : { kind: "none" };
259524
259604
  }
259525
- function markSigned(agent, principal2, pins = keyPins) {
259526
- const pin = pins.read();
259527
- if (pin && pin.agent === agent && pin.principal === principal2 && !pin.signed) pins.write({ ...pin, signed: true });
259605
+ function markSigned(agent, principal2, pins2 = keyPins) {
259606
+ const pin = pins2.read();
259607
+ if (pin && pin.agent === agent && pin.principal === principal2 && !pin.signed) pins2.write({ ...pin, signed: true });
259528
259608
  }
259529
- function disconnect(pins = keyPins) {
259530
- const pin = pins.read();
259531
- pins.clear();
259609
+ function disconnect(pins2 = keyPins) {
259610
+ const pin = pins2.read();
259611
+ pins2.clear();
259532
259612
  return pin;
259533
259613
  }
259534
259614
  function actingWords(acting, face) {
@@ -259604,8 +259684,8 @@ var init_connect = __esm({
259604
259684
  SELF = "self";
259605
259685
  told = /* @__PURE__ */ new Set();
259606
259686
  stderr = (line2) => console.error(line2);
259607
- pinOf = (agent, pins) => {
259608
- const pin = pins.read();
259687
+ pinOf = (agent, pins2) => {
259688
+ const pin = pins2.read();
259609
259689
  return pin && pin.agent === agent ? pin.principal : null;
259610
259690
  };
259611
259691
  feeAllowanceSpent = (error54) => /codespace:?\s*feegrant|fee limit exceeded|fee allowance/i.test(error54 instanceof Error ? error54.message : String(error54));
@@ -291941,7 +292021,7 @@ import { randomBytes as randomBytes7 } from "node:crypto";
291941
292021
  // package.json
291942
292022
  var package_default = {
291943
292023
  name: "@ixo/asks-mcp",
291944
- version: "1.6.0-dev.3",
292024
+ version: "1.6.0-dev.4",
291945
292025
  description: "The Openhands Network MCP server: read the board, answer asks, post work and get paid on the ixo chain, with a key of its own on your machine.",
291946
292026
  license: "Apache-2.0",
291947
292027
  repository: {
@@ -296091,7 +296171,7 @@ ${settings}`
296091
296171
  confirm: external_exports.string().optional().describe("The exact total from the preview. Omit to preview.")
296092
296172
  }
296093
296173
  },
296094
- async ({ name: name8, pins, pin_pay, currency, days, metres, confirm }) => {
296174
+ async ({ name: name8, pins: pins2, pin_pay, currency, days, metres, confirm }) => {
296095
296175
  const acting = await principal2();
296096
296176
  const coin = await coinFor(currency, acting);
296097
296177
  if (typeof coin !== "string") return text4(`Refused: ${coin.refused}. Nothing was opened and nothing was spent.`);
@@ -296112,8 +296192,8 @@ ${settings}`
296112
296192
  terms.via = "mcp";
296113
296193
  const unread = await unreadable(terms);
296114
296194
  if (unread) return text4(unread);
296115
- const { total } = escrowTotal(pin_pay, pins, legBase, coinSymbol, coin === FEE_DENOM);
296116
- const preview = `${face(total, coinSymbol)} to open "${name8}". ${pins} ${pins === 1 ? "report" : "reports"} at ${face(pin_pay, coinSymbol)} each, plus ${feeWords(legBase)} to whoever checks each one. Mapping stays open ${open} days, and two pins closer than ${apart} m are the same mess`;
296195
+ const { total } = escrowTotal(pin_pay, pins2, legBase, coinSymbol, coin === FEE_DENOM);
296196
+ const preview = `${face(total, coinSymbol)} to open "${name8}". ${pins2} ${pins2 === 1 ? "report" : "reports"} at ${face(pin_pay, coinSymbol)} each, plus ${feeWords(legBase)} to whoever checks each one. Mapping stays open ${open} days, and two pins closer than ${apart} m are the same mess`;
296117
296197
  const guard = spendGuard({ bucket: "organise", task: `campaign ${tag}`, total, ...confirm ? { confirm } : {}, what: preview, symbol: coinSymbol });
296118
296198
  if (guard) {
296119
296199
  return text4(
@@ -296138,7 +296218,7 @@ Mapping pays for the report. What happens to a mapped pin afterwards is a separa
296138
296218
  ...await postedAs(acting),
296139
296219
  title: terms.task,
296140
296220
  amountUixo: baseFromPay2(pin_pay),
296141
- quota: pins,
296221
+ quota: pins2,
296142
296222
  days: open,
296143
296223
  denom: coin,
296144
296224
  terms,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ixo/asks-mcp",
3
- "version": "1.6.0-dev.3",
3
+ "version": "1.6.0-dev.4",
4
4
  "description": "The Openhands Network MCP server: read the board, answer asks, post work and get paid on the ixo chain, with a key of its own on your machine.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {