@50arms/platform-mcp 0.1.436 → 0.1.438

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.
@@ -33,7 +33,8 @@ export class HeartbeatReporter {
33
33
  // DX-5088 - when the heartbeat first failed to land in the current outage (across every reconnect); null while the last one landed.
34
34
  outageSince = null;
35
35
  outageAnnounced = false;
36
- // DX-4418 - the stop notice for a revoked key goes out once; every later beat is refused the same way.
36
+ // DX-4418 - the stop notice for a revoked key goes out once per revocation; every later beat is refused the same way. DX-6894: a beat the
37
+ // dashboard accepts again (a person signed the session back in) re-arms it, so a second revocation is announced too.
37
38
  revocationAnnounced = false;
38
39
  constructor(deps, transcriptPath) {
39
40
  this.deps = deps;
@@ -71,6 +72,8 @@ export class HeartbeatReporter {
71
72
  const answer = await sendHeartbeat(this.deps.auth(), this.deps.http, this.deps.connection(), stats, title, statsHeld);
72
73
  if (answer.landed)
73
74
  this.dashboardTitle = answer.title ?? undefined;
75
+ if (answer.landed && answer.revocation === null)
76
+ this.revocationAnnounced = false;
74
77
  // DX-4418 - a person revoked this session's key: tell the session to stop, at once and only once. Its next
75
78
  // dashboard call says the same.
76
79
  if (answer.revocation !== null && !this.revocationAnnounced) {
package/dist/index.js CHANGED
@@ -908,11 +908,20 @@ strictTool("plan_connect",
908
908
  'The `naming` block\'s exact `{Name}: PLAN-NNN {short title}`, after renaming this thread. Omit on the first connect.'),
909
909
  assignment_id: z.number().int().positive().optional().describe("The assignment this connect answers, when a person's assignment message names one. Refused `assignment_ended` once it is over: then do nothing more about it."),
910
910
  disconnect: z.boolean().optional().describe("true = leave `plan_id` (your current plan); no `title`."),
911
+ person_sign_in: z
912
+ .boolean()
913
+ .optional()
914
+ .describe(
915
+ // DX-6894 (DEC-589): set only by the plugin band when a PERSON presses its Sign in; the one call that leaves a revoked key's stop.
916
+ "Set only by the plugin's Sign in button when a person presses it; an agent never sets it. On a session whose key a person revoked it lifts the stop and starts a new access request a person must approve; any call without it still answers the stop."),
911
917
  },
912
918
  // DX-2862 — the connect is also the moment the session's connection record is written for `restart_notice` and the listener reminder: of the
913
919
  // dashboard MCP servers a session may have configured, the one that connected the plan is the one whose credential they
914
920
  // must resolve (the relay needs none of this: it lives in each server, with that server's own credential).
915
921
  async (args) => {
922
+ // DX-6894 — a person's Sign in press leaves the revoked stop; every call without it still answers that stop.
923
+ if (args.person_sign_in === true)
924
+ access.personSignIn();
916
925
  // DX-4390 — a leave names the plan it leaves, and needs a key like any other call.
917
926
  if (args.disconnect === true && args.plan_id === undefined) {
918
927
  return haltResult("plan_connect: `disconnect: true` needs `plan_id`, the plan this session is currently on");
@@ -250,7 +250,7 @@ function resolveToolkitVersion(stamped, readManifest) {
250
250
  return stamped ?? readManifest();
251
251
  }
252
252
  var readOwnManifest = () => JSON.parse(readFileSync2(new URL("../../package.json", import.meta.url), "utf8")).version;
253
- var TOOLKIT_VERSION = resolveToolkitVersion(false ? void 0 : "0.1.436", readOwnManifest);
253
+ var TOOLKIT_VERSION = resolveToolkitVersion(false ? void 0 : "0.1.438", readOwnManifest);
254
254
  function envelope(now) {
255
255
  return { contract_version: CONTRACT_VERSION, toolkit_version: TOOLKIT_VERSION, profile: PROFILE, measured_at: now.toISOString() };
256
256
  }
@@ -75,6 +75,7 @@ export const GET_ROUTE_BOARD_SCOPE = [
75
75
  ["/api/team/quality/gates", "refuse"],
76
76
  ["/api/team/quality/people", "refuse"],
77
77
  ["/api/team/roles", "refuse"],
78
+ ["/api/test-runs/file-times", "refuse"],
78
79
  ["/api/test-runs/stats", "refuse"],
79
80
  ["/api/usage/series", "refuse"],
80
81
  ["/api/users", "refuse"],
@@ -84,6 +85,7 @@ export const GET_ROUTE_BOARD_SCOPE = [
84
85
  ["/api/worker/dispatches/:dispatchId/usage-predecessor", "refuse"],
85
86
  ["/api/worker/dispatches/non-terminal", "refuse"],
86
87
  ["/api/worker/dispatches/recent-signal-failures", "refuse"],
88
+ ["/api/worker/test-runs/file-times", "refuse"],
87
89
  ["/api/workers", "refuse"],
88
90
  ];
89
91
  /** DX-5337 - the routes that read the card's board from `body.board` only: the client fills a dispatch's board in there and sends no `?board=`. */
@@ -8,10 +8,11 @@
8
8
  * (`session-key.ts`) and a 401 on it deletes the file (`dropKey`).
9
9
  * - signed out no credential. Every tool but `plan_connect` halts; `plan_connect` asks.
10
10
  *
11
- * DX-4418: a fourth state, REVOKED, ends all three for good. The dashboard answers `401 key_revoked` to a key a person revoked
12
- * (as opposed to one that lapsed); this object then remembers it for the life of the process, every dashboard tool (and
13
- * `plan_connect`) returns the one stop halt (`key-revoked-halt.ts`), and no new access request is made. A session's key file is
14
- * deleted; a dispatch's credential is left to its dispatch, but the halt is the same.
11
+ * DX-4418: a fourth state, REVOKED, ends all three. The dashboard answers `401 key_revoked` to a key a person revoked
12
+ * (as opposed to one that lapsed); this object then remembers it, every dashboard tool (and `plan_connect`) returns the one stop
13
+ * halt (`key-revoked-halt.ts`), and no new access request is made. A session's key file is deleted; a dispatch's credential is
14
+ * left to its dispatch, but the halt is the same. DX-6894: the one way out is `personSignIn`, the call a PERSON's Sign in press
15
+ * makes (`plan_connect` with `person_sign_in: true`); nothing an agent calls lifts the stop.
15
16
  *
16
17
  * ASKING NEVER BLOCKS FOR MINUTES. The first signed-out `plan_connect` posts the request and
17
18
  * answers at once with the approval URL and confirm code. From then on THIS object polls claim in
@@ -69,7 +70,7 @@ const notReady = (answer, isError, halt) => ({
69
70
  export class SessionAccess {
70
71
  deps;
71
72
  credential;
72
- // DX-4418: set once the dashboard says a person revoked the key; never cleared (a restarted server is a new process).
73
+ // DX-4418: set once the dashboard says a person revoked the key. Only `personSignIn` clears it (DX-6894); a restarted server is a new process.
73
74
  revoked = null;
74
75
  pending = null;
75
76
  // DX-4390: two concurrent signed-out plan_connect calls must not each post a request.
@@ -105,6 +106,19 @@ export class SessionAccess {
105
106
  revokedHalt() {
106
107
  return this.revoked === null ? null : keyRevokedHalt(this.revoked.revocation, this.revoked.dispatched);
107
108
  }
109
+ /**
110
+ * DX-6894 (DEC-589): a person pressed Sign in on a session whose key a person revoked: leave the revoked state so the next ask starts a
111
+ * fresh access request, which a person must approve. Only a session's own key can be signed in again: a dispatch's credential is the
112
+ * dispatch's, so its stop stays. True when the revoked state was left; false when the session was not revoked or holds a dispatch
113
+ * credential (then nothing changes).
114
+ */
115
+ personSignIn() {
116
+ if (this.revoked === null || this.credential !== null)
117
+ return false;
118
+ this.deps.log?.(`[platform-mcp] a person pressed Sign in on a revoked session (revoked by ${this.revoked.revocation.revokedBy} at ${this.revoked.revocation.revokedAt}): asking for access again`);
119
+ this.revoked = null;
120
+ return true;
121
+ }
108
122
  /**
109
123
  * DX-4418: a dashboard answer that says a person revoked the key behind it: remembered for good (a session's key file is
110
124
  * deleted, a dispatch's credential is left to its dispatch) and answered as the stop halt. Null for every other answer.
@@ -196,7 +210,7 @@ export class SessionAccess {
196
210
  return run;
197
211
  }
198
212
  async ensureAccessNow(title) {
199
- // DX-4418: a person revoked this session's access; it never asks again.
213
+ // DX-4418: a person revoked this session's access; it asks again only after `personSignIn` (DX-6894).
200
214
  const revokedHalt = this.revokedHalt();
201
215
  if (revokedHalt !== null)
202
216
  return notReady({ state: "access_revoked", instruction: revokedHalt }, true, revokedHalt);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@50arms/platform-mcp",
3
- "version": "0.1.436",
3
+ "version": "0.1.438",
4
4
  "description": "Stdio MCP server fronting the 50 Arms dashboard API for agents (operator sessions and dispatched workers alike): the generic 50arms_api/50arms_api_list/50arms_api_spec tools reach any route the caller's credential may call, plus dedicated agent-specific tools (plan_connect, request_permission, attach_file, and 50arms_slack_reply) (DX-3465, PLN-11 R-25).",
5
5
  "license": "MIT",
6
6
  "type": "module",