@thehammer/danx-dashboard-mcp 0.1.247 → 0.1.248

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.
@@ -54,8 +54,6 @@ function isCreated(value) {
54
54
  typeof v.secret === "string" &&
55
55
  typeof v.confirmCode === "string" &&
56
56
  typeof v.approvalUrl === "string" &&
57
- typeof v.expiresAt === "string" &&
58
- Number.isFinite(Date.parse(v.expiresAt)) &&
59
57
  typeof v.pollMs === "number");
60
58
  }
61
59
  export async function createAccessRequest(fetchImpl, dashboardUrl, input) {
package/dist/index.js CHANGED
@@ -515,7 +515,8 @@ strictTool("attach_file", "Attach a LOCAL file to a card. This server reads and
515
515
  // ---------------- request_permission (DX-4434, parent DX-4432) ----------------
516
516
  strictTool("request_permission", "Ask a person to grant this key ONE MORE permission. Call it when a call was refused `403 forbidden` naming a " +
517
517
  "`permission`, or when you know you need one more. It returns AT ONCE with `{state, approvalUrl, confirmCode, " +
518
- "expiresAt, instruction}`: show the person the code and URL, and keep working on anything else. If they approve, a " +
518
+ "instruction}`: show the person the code and URL, and keep working on anything else. It waits until this session " +
519
+ "ends; if expired, reconnect and ask again. If they approve, a " +
519
520
  "later call simply succeeds with the same key (no new key, no re-auth). The same permissions asked again while " +
520
521
  "pending return the pending request. A refusal (unknown permission, already held, rate limited) comes back as " +
521
522
  "`{ok:false, status, body}`. If you are blocked without it, raise a problem on the card.", {
@@ -551,7 +552,8 @@ strictTool("plan_connect",
551
552
  // DX-4390 — signed out, this is also how the session asks for access.
552
553
  "Connect THIS session to a plan; one call is enough to start. Signed out, it requests access instead: " +
553
554
  "the reply is `state: approval_required` or `approval_pending` with an `approvalUrl` and `confirmCode` " +
554
- "(show the code, open the URL, then call again). Signed in, the reply carries `missedEvents` (operator " +
555
+ "(show the code, open the URL, then call again; it waits until this session ends; a call re-asks an expired one). " +
556
+ "Signed in, the reply carries `missedEvents` (operator " +
555
557
  "events on the plan that no session had been told about, oldest first: read them first), " +
556
558
  "`browserInstruction` (do it now), `briefing` (the plan's goals, rules, caveats, architecture section " +
557
559
  "titles and a first page of open cards) and `listenerHealth` (when its `state` is not `healthy`, do its " +
@@ -1,13 +1,13 @@
1
- import { POLL_GRACE_MS, defaultSleep } from "./session-access.js";
1
+ import { defaultSleep } from "./session-access.js";
2
2
  import { McpOutdatedError } from "./version-floor.js";
3
3
  const PERMISSION_REQUESTS_BASE_PATH = "/api/permission-requests";
4
4
  const APPROVAL_REQUIRED_INSTRUCTION = "Show the person the approval URL and the confirm code (they check the code matches, then approve). Keep working on anything " +
5
- "else: this call has returned and nothing waits. If they approve, this process picks the grant up in the background and a " +
6
- "later call simply succeeds with the same key (no new key, no re-auth). If you are blocked without it, raise a problem on " +
7
- "the card; a person sees problems in Needs You.";
8
- const APPROVAL_PENDING_INSTRUCTION = "This permission is already asked for and not yet decided. Remind the person of the confirm code and the approval URL; keep " +
9
- "working on anything else. A grant is picked up in the background and a later call simply succeeds with the same key. If you " +
10
- "are blocked without it, raise a problem on the card.";
5
+ "else: this call has returned and nothing waits. The request stays open until this session ends, however long they take. If " +
6
+ "they approve, this process picks the grant up in the background and a later call simply succeeds with the same key (no new " +
7
+ "key, no re-auth). If you are blocked without it, raise a problem on the card; a person sees problems in Needs You.";
8
+ const APPROVAL_PENDING_INSTRUCTION = "This permission is already asked for and not yet decided; the request stays open until this session ends. Remind the person " +
9
+ "of the confirm code and the approval URL; keep working on anything else. A grant is picked up in the background and a later " +
10
+ "call simply succeeds with the same key. If you are blocked without it, raise a problem on the card.";
11
11
  /** The dedupe set: sorted and de-duplicated, so the order the agent listed them in never matters. */
12
12
  function setOf(permissions) {
13
13
  return [...new Set(permissions)].sort();
@@ -19,8 +19,6 @@ function isCreated(value) {
19
19
  return (typeof v.publicId === "string" &&
20
20
  typeof v.confirmCode === "string" &&
21
21
  typeof v.approvalUrl === "string" &&
22
- typeof v.expiresAt === "string" &&
23
- Number.isFinite(Date.parse(v.expiresAt)) &&
24
22
  typeof v.pollMs === "number");
25
23
  }
26
24
  /** A 429's own `retryAfterMs`, else 0 (the poll then waits its normal interval). */
@@ -32,11 +30,9 @@ export class PermissionRequests {
32
30
  deps;
33
31
  pending = new Map();
34
32
  queue = Promise.resolve();
35
- now;
36
33
  sleep;
37
34
  constructor(deps) {
38
35
  this.deps = deps;
39
- this.now = deps.now ?? Date.now;
40
36
  this.sleep = deps.sleep ?? defaultSleep;
41
37
  }
42
38
  ask(args) {
@@ -72,12 +68,11 @@ export class PermissionRequests {
72
68
  state,
73
69
  approvalUrl: request.approvalUrl,
74
70
  confirmCode: request.confirmCode,
75
- expiresAt: request.expiresAt,
76
71
  instruction: state === "approval_required" ? APPROVAL_REQUIRED_INSTRUCTION : APPROVAL_PENDING_INSTRUCTION,
77
72
  };
78
73
  }
79
74
  /**
80
- * Polls until the request is decided or lapsed, then frees its set so a later call may ask again. Every ending is final: a
75
+ * Polls until the request is decided or expired, then frees its set so a later call may ask again. Every ending is final: a
81
76
  * halt (revoked, signed out, key lapsed) stops it silently, since the session no longer holds a key that could be granted.
82
77
  */
83
78
  async pollUntilDecided(key, request) {
@@ -92,21 +87,19 @@ export class PermissionRequests {
92
87
  this.pending.delete(key);
93
88
  }
94
89
  }
90
+ // DX-4530: no give-up time - the request is open until it is decided or the key dies (the `guard` halt).
95
91
  async poll(request) {
96
- const giveUpAt = Date.parse(request.expiresAt) + POLL_GRACE_MS;
97
92
  let waitMs = request.pollMs;
98
93
  for (;;) {
99
94
  await this.sleep(waitMs);
100
95
  waitMs = request.pollMs;
101
- if (this.now() > giveUpAt)
102
- return;
103
96
  let guarded;
104
97
  try {
105
98
  // `guard` runs before EVERY claim (DX-4418): a revoked or signed-out session answers its halt without a request.
106
99
  guarded = await this.deps.access.guard(() => this.deps.request({ method: "POST", basePath: PERMISSION_REQUESTS_BASE_PATH, path: `/${request.publicId}/claim` }));
107
100
  }
108
101
  catch (err) {
109
- // An outdated MCP will never get a different answer; any other failure is retried until the request lapses.
102
+ // An outdated MCP will never get a different answer; any other failure is retried until the request ends.
110
103
  if (err instanceof McpOutdatedError)
111
104
  throw err;
112
105
  this.deps.log?.(`[danx-dashboard-mcp] permission request poll failed, retrying: ${err.message}`);
@@ -120,7 +113,7 @@ export class PermissionRequests {
120
113
  continue;
121
114
  }
122
115
  if (claim.status === 404)
123
- return; // unknown or already reaped: nothing left to wait for
116
+ return; // unknown or long since deleted: nothing left to wait for
124
117
  if (!claim.ok) {
125
118
  throw new Error(`the dashboard refused the claim (${claim.status}): ${JSON.stringify(claim.body)}`);
126
119
  }
@@ -18,6 +18,10 @@
18
18
  * the background, so the key is stored the moment the user approves, and the next `plan_connect`
19
19
  * waits at most `awaitMs` (about 45s) for it before answering `approval_pending`. The secret the
20
20
  * poll needs lives only in this process's memory; a restarted session asks again.
21
+ *
22
+ * DX-4530: a request has no deadline. It stays open for as long as this process keeps polling it, so it waits for a person
23
+ * however long they take, and it expires once the session ends (its polls stop). A poll that cannot reach the dashboard is
24
+ * retried; the dashboard answers `expired` if they stopped arriving for longer than its requester-gone window.
21
25
  */
22
26
  import { existsSync } from "node:fs";
23
27
  import { hostname } from "node:os";
@@ -28,8 +32,6 @@ import { deleteSessionKey, writeSessionKey } from "./session-key.js";
28
32
  import { McpOutdatedError } from "./version-floor.js";
29
33
  /** Longest a `plan_connect` call waits for an approval already asked for. */
30
34
  export const AWAIT_APPROVAL_MS = 45_000;
31
- /** The server reaps a lapsed request about a minute after expiry; polling past that finds nothing. */
32
- export const POLL_GRACE_MS = 90_000;
33
35
  /** A live server rewrites its key file this often, so an in-use key never ages out of the 7-day prune. */
34
36
  const KEEP_KEY_FILE_EVERY_MS = 60 * 60 * 1000;
35
37
  /** The request's title when the agent gave none: the session id's first characters. */
@@ -39,9 +41,10 @@ export const SIGN_IN_HALT = "Not signed in to the danxbot dashboard. Call `plan_
39
41
  export const KEY_LAPSED_HALT = "The dashboard no longer accepts this session's key (it lapsed after a day unused), so this session is signed out. " +
40
42
  SIGN_IN_HALT;
41
43
  const APPROVAL_REQUIRED_INSTRUCTION = "Show the user the confirm code and open the approval URL in their browser; they check the code " +
42
- "matches and approve. Then call `plan_connect` again: it waits up to about 45 seconds for the approval.";
43
- const APPROVAL_PENDING_INSTRUCTION = "Still waiting for the user to approve. Remind them of the confirm code and the URL, then call " +
44
- "`plan_connect` again.";
44
+ "matches and approve. Then call `plan_connect` again: it waits up to about 45 seconds for the approval. " +
45
+ "The request stays open until this session ends, however long the user takes.";
46
+ const APPROVAL_PENDING_INSTRUCTION = "Still waiting for the user to approve; the request stays open until this session ends. Remind them of the " +
47
+ "confirm code and the URL, then call `plan_connect` again.";
45
48
  const DENIED_INSTRUCTION = "The user denied the request. Do not retry unless they ask; a new `plan_connect` makes a new request.";
46
49
  const DASHBOARD_OUTDATED_ERROR = "this dashboard does not support request-and-approve yet (POST /api/access-requests answered 404): " +
47
50
  "it must be updated before this session can sign in";
@@ -50,7 +53,8 @@ const unknownTeamError = (team) => `the dashboard has no team "${team}" (DANXBOT
50
53
  const NO_SESSION_ID_ERROR = "this server has no CLAUDE_CODE_SESSION_ID, so it cannot request a session key";
51
54
  const CLAIMED_LOST_ERROR = "the dashboard says this request was already claimed, but this process never received the key; " +
52
55
  "start a new request";
53
- const EXPIRED_NOTE = "The previous request expired before it was approved.";
56
+ const EXPIRED_NOTE = "The previous request expired before it was approved: the dashboard stopped hearing from this session. This is a new " +
57
+ "request: show the user its new confirm code and URL.";
54
58
  export const defaultSleep = (ms) => new Promise((resolve) => {
55
59
  setTimeout(resolve, ms).unref();
56
60
  });
@@ -233,24 +237,21 @@ export class SessionAccess {
233
237
  state,
234
238
  approvalUrl: request.approvalUrl,
235
239
  confirmCode: request.confirmCode,
236
- expiresAt: request.expiresAt,
237
240
  instruction: state === "approval_required" ? APPROVAL_REQUIRED_INSTRUCTION : APPROVAL_PENDING_INSTRUCTION,
238
241
  };
239
242
  }
243
+ // DX-4530: no give-up time - the polls are what keep the request open, until the dashboard answers how it ended.
240
244
  async pollUntilSettled(request) {
241
- const giveUpAt = Date.parse(request.expiresAt) + POLL_GRACE_MS;
242
245
  let waitMs = request.pollMs;
243
246
  for (;;) {
244
247
  await this.sleep(waitMs);
245
248
  waitMs = request.pollMs;
246
- if (this.now() > giveUpAt)
247
- return { kind: "expired" };
248
249
  let claim;
249
250
  try {
250
251
  claim = await claimAccessRequest(this.fetchImpl, this.deps.dashboardUrl, request.secret);
251
252
  }
252
253
  catch (err) {
253
- // A claim the dashboard could not answer is retried until the request lapses; an outdated
254
+ // A claim the dashboard could not answer is retried until it answers how the request ended; an outdated
254
255
  // MCP will never get a different answer, so that one ends it.
255
256
  if (err instanceof McpOutdatedError)
256
257
  return { kind: "failed", error: err.message };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.247",
3
+ "version": "0.1.248",
4
4
  "description": "Stdio MCP server fronting danxbot's dashboard API for agents (operator sessions and dispatched workers alike): the generic danxbot_api/danxbot_api_list/danxbot_api_spec tools reach any route the caller's credential may call, plus dedicated agent-specific tools (plan_connect, danxbot_complete, and other per-dispatch lifecycle callbacks) (DX-3465, PLN-11 R-25).",
5
5
  "license": "MIT",
6
6
  "type": "module",