@50arms/platform-mcp 0.1.443 → 0.1.444
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 +12 -12
- package/dist/credential.js +3 -3
- package/dist/handlers.js +16 -41
- package/dist/http-client.js +2 -2
- package/dist/index.js +50 -88
- package/dist/key-revoked-halt.js +1 -1
- package/dist/listen.js +1 -1
- package/dist/machine-secret.js +1 -1
- package/dist/machine-toolkit.js +1 -1
- package/dist/permission-watch.js +1 -1
- package/dist/restart-notice.js +2 -2
- package/dist/result-limit.js +1 -1
- package/dist/role-instruction-file.js +1 -1
- package/dist/session-access.js +15 -15
- package/dist/session-connection.js +6 -6
- package/dist/session-title.js +1 -1
- package/dist/stop-fixes.js +14 -14
- package/dist/tool-access.js +3 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @50arms/platform-mcp
|
|
2
2
|
|
|
3
|
-
Stdio MCP server fronting the 50 Arms dashboard HTTP API. Since DX-3465 it exposes three generic tools (`50arms_api`, `50arms_api_list`, `50arms_api_spec` — see "Tool surface" below) that reach any route the caller's credential may reach, plus a few dedicated tools for what no route call can do (`
|
|
3
|
+
Stdio MCP server fronting the 50 Arms dashboard HTTP API. Since DX-3465 it exposes three generic tools (`50arms_api`, `50arms_api_list`, `50arms_api_spec` — see "Tool surface" below) that reach any route the caller's credential may reach, plus a few dedicated tools for what no route call can do (`session_connect`, `request_permission`, `attach_file`, `50arms_slack_reply`) — there is no per-operation tool any more, and since DX-4525 no dispatch-callback tool either: `50arms_api` is every agent's primary channel to the dashboard (ending a dispatch, gate sign-off, Slack update, thread link, Slack recovery, evaluator summary and template-app load/save are all `50arms_api` calls to the routes in `src/dispatch/callback-routes.ts`). The server instructions and the `50arms_api_spec` description tell an agent to read the spec before its first call to a route it has not called before.
|
|
4
4
|
|
|
5
5
|
`50arms_api` is Zod-validated at the MCP boundary, fetches under the hood, and passes the server response back to the agent verbatim. Refusal envelopes (`{error, ...extra}` with `failed_gate`, `non_terminal_phases`, `offending_keys`, etc.) come through as `{ok: false, status, body}` so the agent can pick the right next action without guessing. 5xx and network failures throw.
|
|
6
6
|
|
|
@@ -9,20 +9,20 @@ Stdio MCP server fronting the 50 Arms dashboard HTTP API. Since DX-3465 it expos
|
|
|
9
9
|
| Var | Purpose |
|
|
10
10
|
|---|---|
|
|
11
11
|
| `DANXBOT_DASHBOARD_URL` | Dashboard base, e.g. `http://danxbot-dashboard:5555` (inside compose) or `http://localhost:5555` (host). An operator session gets it from the 50arms plugin: its `dashboard_url` option (default the hosted dashboard), chosen per session folder: see the repo's `.claude/rules/dashboard-api-access.md`, the one owner of that rule |
|
|
12
|
-
| `DANX_DASHBOARD_CREDENTIAL` | A DISPATCH's credential: the bearer verbatim, substituted into the materialized `.mcp.json` from its overlay (DX-2862, `src/credential.ts`). Authoritative when set; it never signs out. An operator session sets nothing: with no credential the server boots SIGNED OUT, every tool except `
|
|
13
|
-
| `DANXBOT_TEAM` | OPTIONAL, non-secret (DX-4446): the team slug (or id) whose approvers see this session's access request in their danxbot site. Set it in your shell or a repo's committed `.claude/settings.json` `env` block. Unset, the request is answered by link only. A value that is not a team slug fails at boot; a team the dashboard does not know makes `
|
|
12
|
+
| `DANX_DASHBOARD_CREDENTIAL` | A DISPATCH's credential: the bearer verbatim, substituted into the materialized `.mcp.json` from its overlay (DX-2862, `src/credential.ts`). Authoritative when set; it never signs out. An operator session sets nothing: with no credential the server boots SIGNED OUT, every tool except `session_connect` returns one "not signed in" halt, and `session_connect` requests access (below). `restart_notice` uses the same resolver, so a session's tools and its predecessor lookups can never sign in as two different identities; the relay in this server (`plan_events_wait`) signs in with the server's own credential |
|
|
13
|
+
| `DANXBOT_TEAM` | OPTIONAL, non-secret (DX-4446): the team slug (or id) whose approvers see this session's access request in their danxbot site. Set it in your shell or a repo's committed `.claude/settings.json` `env` block. Unset, the request is answered by link only. A value that is not a team slug fails at boot; a team the dashboard does not know makes `session_connect` answer `unknown_team` (fix `DANXBOT_TEAM`), never a silent link fallback |
|
|
14
14
|
| `DANXBOT_BOARD_ID` | OPTIONAL (DX-4555, DX-5512): the dispatch's board, the board's ref, the positive integer that numbers it within its team (never its database id; the dispatch injects it). Anything else exits at boot. Unset (the 50arms plugin's own server) the server has no default board: a call that needs a board must carry its `board` argument or the dashboard answers 400 (plan and plan-session routes take their boards from the plan and need none) |
|
|
15
15
|
|
|
16
16
|
BOARD-ONLY (DX-1171): board is the first-level concept; repo is DERIVED from board server-side, never passed. At boot the package reads the dispatch's board ref from `DANXBOT_BOARD_ID` and appends it as `?board=<ref>` (and `body.board` on create) to every `/api/issues/*` request, except `GET /api/issues` (the list), whose board is OPTIONAL (DX-4542): there it sends a board only when the caller names one, because no board is the team's whole list across every board. Each tool exposes an optional `board` arg - a board ref, the number (or its decimal text) - that overrides it per-call for cross-board reads/writes (unknown board -> 404). The dashboard owns board->repo resolution; this package never sends `repo`.
|
|
17
17
|
|
|
18
18
|
## Signing in (DX-4390): request and approve
|
|
19
19
|
|
|
20
|
-
An operator session has no stored credential. Signed out, `
|
|
21
|
-
- **A machine secret and an auto-approved request (DX-6764, PLAN-128).** Every `session` access request also sends `machineSecret`: one random secret per machine and per dashboard, made the first time it is needed in `~/.config/danxbot/machine-secrets/<sha12(dashboard url)>.json` (mode 0600, written durably, then hard-linked into place so two sessions starting at once on a fresh machine end up with the same one; an unusable file is a loud error, never replaced). The dashboard keeps only its hash, so a person can let later sessions from this machine sign in by themselves; it is never sent to another dashboard, and the `machine` daemon's request and a dispatched agent's credential are unchanged. When the create answer says `autoApproved: true` no person decides the request: the first claim is not delayed, `
|
|
22
|
-
- **A launch token (DX-6801, DEC-522).** `
|
|
20
|
+
An operator session has no stored credential. Signed out, `session_connect` (optionally with `title`, the session's own Claude title) posts `POST /api/access-requests` and answers at once `{state: "approval_required", approvalUrl, confirmCode, expiresAt, instruction}`: show the user the code and open the URL; they approve in the dashboard. The server polls `POST /api/access-requests/claim` in the background, so the key is stored when the approval lands; the next `session_connect` waits up to about 45 seconds for it, then connects, or answers `approval_pending`. One tool call never blocks for minutes. `denied` is reported once; a request that lapsed (the dashboard deletes it within about a minute of expiry, so a claim answers 404) starts a fresh one on the next call.
|
|
21
|
+
- **A machine secret and an auto-approved request (DX-6764, PLAN-128).** Every `session` access request also sends `machineSecret`: one random secret per machine and per dashboard, made the first time it is needed in `~/.config/danxbot/machine-secrets/<sha12(dashboard url)>.json` (mode 0600, written durably, then hard-linked into place so two sessions starting at once on a fresh machine end up with the same one; an unusable file is a loud error, never replaced). The dashboard keeps only its hash, so a person can let later sessions from this machine sign in by themselves; it is never sent to another dashboard, and the `machine` daemon's request and a dispatched agent's credential are unchanged. When the create answer says `autoApproved: true` no person decides the request: the first claim is not delayed, `session_connect` waits up to about 45 seconds for the key and answers signed in with no `approvalUrl` or `confirmCode`; a claim slower than that answers `{state: "signing_in"}`, still with no link. `autoApproved` is required in the create answer: a dashboard that does not send it fails the request loudly. A request left pending behaves as above.
|
|
22
|
+
- **A launch token (DX-6801, DEC-522).** `session_connect` takes an optional `launch_token`: the one-use token the session that started this one passed in its prompt. A signed-out session given one redeems it once per process through the public `POST /api/access-requests/claim` with `{secret: <token>, labelId: <this session's id>}` (`labelId` is sent only on a redeem); `approved` stores the key like an approval and answers signed in, with no access request. Any other answer (`claimed`, `expired`, 404, 409 `launch_grant_refused` with its `reason`, 429) spends the token and falls through to the usual person's request, the answer carrying a `note` that names why the token was not used. A signed-in session ignores the token.
|
|
23
23
|
|
|
24
24
|
- **Storage.** `~/.config/danxbot/session-keys/<sha12(dashboard url)>/<session id>.json`, mode 0600, written write-then-rename, pruned after 7 days. One file per dashboard and per session; this server reads it at boot, and `restart_notice` reads the same file through the `session-key` credential source and proves the fingerprint. The key lapses on the dashboard after 24 hours without use (every authenticated call, the relay's heartbeat included, renews it), and is revocable from the account page.
|
|
25
|
-
- **A 401 on the stored key** deletes the file and returns the sign-in halt (in `
|
|
25
|
+
- **A 401 on the stored key** deletes the file and returns the sign-in halt (in `session_connect`: a new request in the same call). That is a key that lapsed. A key a PERSON revoked is answered `401 key_revoked` (DX-4418): the key file is deleted and EVERY dashboard tool, `session_connect` included, returns one stop halt (who revoked it and when; commit work in progress and end; a dispatched agent makes no further call), for the rest of the process. No access request is made. The worker finds the revocation itself and ends the dispatch as revoked (every dashboard route, the dispatch-complete one included, refuses a revoked key). `plan_events_wait` answers `stopped` with the reason `key_revoked` the same way. **A 404 on create** means the dashboard predates request-and-approve and says so.
|
|
26
26
|
- A dispatch's `DANX_DASHBOARD_CREDENTIAL` skips all of this. There is no token-file credential source.
|
|
27
27
|
|
|
28
28
|
### Asking for one more permission (DX-4434)
|
|
@@ -35,11 +35,11 @@ The hint: every dashboard result that is `403 {error: "forbidden", permission}`
|
|
|
35
35
|
|
|
36
36
|
A session is shown only the tools it can call. `src/tool-access.ts` declares, for every tool, what makes it usable, and its one function `toolsFor({permissions, callbacks})` maps what a session holds to the tool names it advertises. `src/tool-surface.ts` applies it with the SDK's `tool.enable()` / `disable()`, so `notifications/tools/list_changed` is sent and Claude Code refetches; nothing else decides visibility, and the danxbot service's budgets (`src/tokens/mcp-surface.ts`) read the same function.
|
|
37
37
|
|
|
38
|
-
- **Bootstrap set**, for every session and the whole of a signed-out one: `
|
|
38
|
+
- **Bootstrap set**, for every session and the whole of a signed-out one: `session_connect`, `request_permission` and the `50arms_api` trio.
|
|
39
39
|
- **Permission tools** (`attach_file` needs `cards.edit` for both targets): shown once the key holds the permission the tool's dashboard route needs.
|
|
40
40
|
- **Callback tool** (`50arms_slack_reply`, the only one, DX-4525): shown when the worker injected its URL (`DANXBOT_SLACK_REPLY_URL`, `<dashboard>/api/slack/reply`, no dispatch id) AND the key holds `slack.threads.post`, so a non-Slack dispatch never sees it. It posts with the dispatch's own credential (`DANX_DASHBOARD_CREDENTIAL`), the same one `50arms_api` uses.
|
|
41
|
-
- **When it is applied.** At boot, as a session with no key; then, if the session holds a key (a dispatch's env credential, or a stored session key), right away from `GET /api/auth/permissions`; a dispatch retries that read three times and fails the boot rather than start without its tools. After `
|
|
42
|
-
- A stream the dashboard refuses (any 4xx) is logged and ends the watch; that session's grants then show their tools at its next `
|
|
41
|
+
- **When it is applied.** At boot, as a session with no key; then, if the session holds a key (a dispatch's env credential, or a stored session key), right away from `GET /api/auth/permissions`; a dispatch retries that read three times and fails the boot rather than start without its tools. After `session_connect` signs a session in. And again when a person approves a `request_permission` grant: the dashboard ends the key's open `/api/stream` with `event: end {reason: "permissions_changed"}`, this process holds one stream open on the key-self topic `key:self` (`src/permission-watch.ts`; any key may subscribe with no permission, because a dispatch's agent role holds no read-floor key), and after EVERY (re)connect re-reads the key's permissions (a failed read is retried with bounded backoff) and enables what is newly allowed in ONE `list_changed`. It reconnects with backoff after a drop, and stops when stdin closes. A grant that adds no tool, and a repeated read, switch nothing and send no notification, because a changed tool list costs the session its prompt cache.
|
|
42
|
+
- A stream the dashboard refuses (any 4xx) is logged and ends the watch; that session's grants then show their tools at its next `session_connect`. A permission read the dashboard refuses (401/403, other than the Terms halt) fails a dispatch's boot rather than quietly narrowing its tools.
|
|
43
43
|
|
|
44
44
|
## Tool surface (DX-3465 — one generic tool, not one per operation)
|
|
45
45
|
|
|
@@ -59,7 +59,7 @@ All exposed as `mcp__platform__<name>` once wired through the workspace `mcp.tem
|
|
|
59
59
|
|
|
60
60
|
There is no dedicated `resource_fields` tool (removed DX-3465 — it was a thin wrapper `50arms_api` already reaches with no agent-specific behavior earning it its own tool). What an opt-in field TREE may name for one resource (`{resource, description, always, hashes, fields, relations}`) is `50arms_api({method:"GET", path:"/api/resources/<resource>/fields"})` — install-global, no `board`; `resource: "issue"` is the root, and a relation's own `resource` in the response is what to call this again with, one level deeper.
|
|
61
61
|
|
|
62
|
-
A dedicated tool survives only for something no `50arms_api` call can do, never a card/plan/dispatch-callback operation: `request_permission` (asks for a permission its key lacks), `
|
|
62
|
+
A dedicated tool survives only for something no `50arms_api` call can do, never a card/plan/dispatch-callback operation: `request_permission` (asks for a permission its key lacks), `session_connect` (DX-6917: binds the session to ONE plan (`target: "PLAN-n"`) or ONE card on no plan (`target: "<card id>"`, DX-5553), or lets it go with `disconnect: true`; `POST|DELETE /api/plan-sessions/me/connect`, which resolves the target; the plugin's relay loop starts on it and carries the plan's, or the card's and its descendants', events), and the two that read LOCAL files, `attach_file` and `50arms_slack_reply` (`files[]`, see `src/lifecycle.ts`). The live list is always `tool-defs.json` in this package, regenerated by `npm run gen-tool-defs` — read it rather than trusting a list copied into prose.
|
|
63
63
|
|
|
64
64
|
Card and plan reads/writes go through `50arms_api` against the issue/plan route families documented in `GET /api/openapi` (via `50arms_api_spec`) — `POST /api/issues`, `GET /api/issues/:id` (or `/batch` for many at once, DX-2727), `PATCH /api/issues/:id/edit`, `POST /api/issues/:id/transition`, the problems/solutions/dependencies/comments/quality-gates/retro sub-resource routes, and the equivalent `/api/plans/*` family (`GET /api/plans/:planId` — cheap scalars by default, `fields` opts into `cards`/`records`/`architecture`/`session_history`/`worker_dispatches`; `GET /api/plans` for the list, with an optional `status` filter over the same computed `complete`/`awaiting-session`/`building`/`planning` value `GET /api/plans/:planId` returns when asked: `status` is opt-in, so request it with `fields={status:true}`).
|
|
65
65
|
|
|
@@ -85,7 +85,7 @@ The contract the plugin builds against:
|
|
|
85
85
|
|
|
86
86
|
## `restart_notice` — what a restarted session was working on (DX-3928, DX-4632)
|
|
87
87
|
|
|
88
|
-
A tool for the 50arms plugin's session-start hook, not for a model, advertised only on a person's session (never a dispatch) and **usable before sign-in**: a restarted Claude Code session gets a new session id, no plan binding and, until a person approves its first call, no dashboard key of its own, so nothing else could tell it that the plan it had been working was unattended. `
|
|
88
|
+
A tool for the 50arms plugin's session-start hook, not for a model, advertised only on a person's session (never a dispatch) and **usable before sign-in**: a restarted Claude Code session gets a new session id, no plan binding and, until a person approves its first call, no dashboard key of its own, so nothing else could tell it that the plan it had been working was unattended. `session_connect` writes the session's working directory into its connection record (`projectKey`, record schema v2); `restart_notice` finds the newest OTHER sessions' records for the same project (at most ten, each within `STALE_RECORD_MS`), resolves each record's own credential, and asks `GET /api/plan-sessions/<predecessor>/restart-notice` which plan it was on and how many operator events wait there (the connect digest's own query, read-only). No arguments. Answers one of:
|
|
89
89
|
|
|
90
90
|
- `{notice: "<text>"}`: say it (the registry's `plan_restart.waiting_events` text; this package carries no wording).
|
|
91
91
|
- `{notice: null}`: nothing to say (no earlier session of this project connected a plan, the plan is archived or deleted, the record is stale, or every predecessor is still live).
|
package/dist/credential.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* substitutes it into the materialized `.mcp.json` from the dispatch overlay. Authoritative
|
|
15
15
|
* when set, and it never signs out.
|
|
16
16
|
* - a SESSION KEY (`session-key.ts`) - the key a user approved for this one session through
|
|
17
|
-
* `
|
|
18
|
-
* signed out, and `
|
|
17
|
+
* `session_connect`. An operator session has no other credential: with neither of these it boots
|
|
18
|
+
* signed out, and `session_connect` asks for access.
|
|
19
19
|
*
|
|
20
20
|
* There is no token-file source (DX-4390): a long-lived bearer placed on disk by hand is exactly
|
|
21
21
|
* what request-and-approve replaces.
|
|
@@ -60,7 +60,7 @@ export function readCredential(source, env) {
|
|
|
60
60
|
return readSessionKeyFile(source.path);
|
|
61
61
|
}
|
|
62
62
|
catch (err) {
|
|
63
|
-
throw new CredentialError(`cannot read ${describeCredentialSource(source)}: ${err.message}`, "call
|
|
63
|
+
throw new CredentialError(`cannot read ${describeCredentialSource(source)}: ${err.message}`, "call session_connect to request access again; the key file is written when the request is approved");
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
/**
|
package/dist/handlers.js
CHANGED
|
@@ -23,25 +23,24 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { posix, win32 } from "node:path";
|
|
25
25
|
import { readFile, stat } from "node:fs/promises";
|
|
26
|
-
// ----------------
|
|
26
|
+
// ---------------- session_connect (DX-2683, DX-5553, DX-6917) ----------------
|
|
27
27
|
// DX-3465 (PLN-11 R-25) — every other per-operation handler that used to
|
|
28
|
-
// live in this file (issue_*, plan_
|
|
28
|
+
// live in this file (issue_*, plan_*, brief_*,
|
|
29
29
|
// repo_knowledge_*, failure_category_*, dispatch_transcript_search,
|
|
30
30
|
// quality_gate_instruction) is deleted: `danxbotApi`/`danxbotApiSpec` below
|
|
31
|
-
// are the generic replacement. `
|
|
31
|
+
// are the generic replacement. `session_connect` stays a dedicated tool because
|
|
32
32
|
// it is session-bound (see `index.ts`'s own comment on this).
|
|
33
33
|
const PLAN_SESSIONS_BASE_PATH = "/api/plan-sessions";
|
|
34
34
|
/**
|
|
35
|
-
* Connect THIS session to a plan
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* caller's own session.
|
|
35
|
+
* Connect THIS session to a plan or to ONE stand-alone card (a card on no plan, DX-5553), or let it go: `POST|DELETE /api/plan-sessions/me/connect`. `me` in the URL
|
|
36
|
+
* is resolved from the forwarded session id, so this can only ever bind the caller's own session. The same writes the operator's Connect action performs
|
|
37
|
+
* for a plan, reaching the same server-side code path.
|
|
39
38
|
*
|
|
40
|
-
* A session already on another plan is MOVED, and the response says which
|
|
41
|
-
*
|
|
39
|
+
* A session already on another plan or card is MOVED, and the response says which
|
|
40
|
+
* it left (`movedFrom` / `leftPlan` / `leftCard`). The answer carries what the session missed (`missedEvents`) and the exact thread title to take (`naming`).
|
|
42
41
|
*
|
|
43
|
-
* IT DOES NOTHING ABOUT HEARING THE
|
|
44
|
-
* to the
|
|
42
|
+
* IT DOES NOTHING ABOUT HEARING THE EVENTS, deliberately. Delivery belongs
|
|
43
|
+
* to the event relay inside this MCP server (`plan_events_wait`, `plan-events-relay.ts`),
|
|
45
44
|
* which holds the session's ONE listener ticket and is started by the 50arms plugin's loop once this tool
|
|
46
45
|
* succeeds. The dashboard keeps one ticket per session, so a ticket minted here
|
|
47
46
|
* would end the relay's stream.
|
|
@@ -53,25 +52,20 @@ const PLAN_SESSIONS_BASE_PATH = "/api/plan-sessions";
|
|
|
53
52
|
* The event relay keeps it current afterwards, from the transcript (DX-4461,
|
|
54
53
|
* `session-title.ts`).
|
|
55
54
|
*/
|
|
56
|
-
export async function
|
|
55
|
+
export async function sessionConnect(client, args) {
|
|
57
56
|
if (args.disconnect === true) {
|
|
58
57
|
if (args.title !== undefined) {
|
|
59
|
-
return { ok: false, status: 400, body: { error: "
|
|
58
|
+
return { ok: false, status: 400, body: { error: "session_connect: `disconnect: true` takes no `title` — a leave changes no title" } };
|
|
60
59
|
}
|
|
61
|
-
// DX-2985 — `DELETE /api/plan-sessions/me/
|
|
62
|
-
return client.request({
|
|
63
|
-
method: "DELETE",
|
|
64
|
-
path: "/me/plan",
|
|
65
|
-
basePath: PLAN_SESSIONS_BASE_PATH,
|
|
66
|
-
query: { plan_id: args.plan_id },
|
|
67
|
-
});
|
|
60
|
+
// DX-2985 — `DELETE /api/plan-sessions/me/connect?target=`: `me` is the forwarded session id, so only the caller's own session leaves.
|
|
61
|
+
return client.request({ method: "DELETE", path: "/me/connect", basePath: PLAN_SESSIONS_BASE_PATH, query: { target: args.target } });
|
|
68
62
|
}
|
|
69
63
|
return client.request({
|
|
70
64
|
method: "POST",
|
|
71
|
-
path: "/me/
|
|
65
|
+
path: "/me/connect",
|
|
72
66
|
basePath: PLAN_SESSIONS_BASE_PATH,
|
|
73
67
|
body: {
|
|
74
|
-
|
|
68
|
+
target: args.target,
|
|
75
69
|
...(args.title === undefined ? {} : { title: args.title }),
|
|
76
70
|
...(args.assignment_id === undefined ? {} : { assignment_id: args.assignment_id }),
|
|
77
71
|
},
|
|
@@ -85,25 +79,6 @@ export async function planConnect(client, args) {
|
|
|
85
79
|
export async function claimSessionAgent(client) {
|
|
86
80
|
return client.request({ method: "POST", path: "/me/agent", basePath: PLAN_SESSIONS_BASE_PATH });
|
|
87
81
|
}
|
|
88
|
-
/**
|
|
89
|
-
* DX-5553 - hold ONE stand-alone card (a card on no plan) instead of a plan, or let it go: `POST|DELETE /api/plan-sessions/me/card`. `me` is the forwarded session id, so only
|
|
90
|
-
* the caller's own session is ever bound. Like `plan_connect`, the answer carries what the session missed (`missedEvents`) and the exact thread title to take (`naming`); the
|
|
91
|
-
* session then hears that card's and its descendants' events through the same relay as a plan's.
|
|
92
|
-
*/
|
|
93
|
-
export async function cardConnect(client, args) {
|
|
94
|
-
if (args.disconnect === true) {
|
|
95
|
-
if (args.title !== undefined) {
|
|
96
|
-
return { ok: false, status: 400, body: { error: "card_connect: `disconnect: true` takes no `title` — a leave changes no title" } };
|
|
97
|
-
}
|
|
98
|
-
return client.request({ method: "DELETE", path: "/me/card", basePath: PLAN_SESSIONS_BASE_PATH, query: { card_id: args.card_id } });
|
|
99
|
-
}
|
|
100
|
-
return client.request({
|
|
101
|
-
method: "POST",
|
|
102
|
-
path: "/me/card",
|
|
103
|
-
basePath: PLAN_SESSIONS_BASE_PATH,
|
|
104
|
-
body: { card_id: args.card_id, ...(args.title === undefined ? {} : { title: args.title }), ...(args.assignment_id === undefined ? {} : { assignment_id: args.assignment_id }) },
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
82
|
// DX-3465 (code-architecture gate finding, at 275f82c0d) — the dedicated
|
|
108
83
|
// `resource_fields` handler (DX-3426) was deleted from here: it was a thin
|
|
109
84
|
// wrapper around `GET /api/resources/:resource/fields`, which `50arms_api`
|
package/dist/http-client.js
CHANGED
|
@@ -60,7 +60,7 @@ export class DashboardHttpClient {
|
|
|
60
60
|
//
|
|
61
61
|
// NO TITLE HEADER (DX-2816, revised). This server has no way to read
|
|
62
62
|
// Claude's own session title — see `readSessionConfig` in `index.ts`.
|
|
63
|
-
// The title travels instead as `
|
|
63
|
+
// The title travels instead as `session_connect`'s own `title` argument,
|
|
64
64
|
// straight in that call's body, because the AGENT (not this process) is
|
|
65
65
|
// the one thing able to read it.
|
|
66
66
|
if (this.config.session) {
|
|
@@ -158,7 +158,7 @@ export class DashboardHttpClient {
|
|
|
158
158
|
// DX-4592: a board-less server (the 50arms plugin's, no DANXBOT_BOARD_ID) stamps nothing when the call names no
|
|
159
159
|
// board. Whether the route NEEDS one is the dashboard's to say: a route that does (quality-gate and settings routes, the dispatch
|
|
160
160
|
// verbs) answers 400 naming `board`, while a card-id, plan or plan-session route takes its board from the card
|
|
161
|
-
// or the plan. Refusing here (DX-4555) wrongly stopped every plan route, `
|
|
161
|
+
// or the plan. Refusing here (DX-4555) wrongly stopped every plan route, `session_connect` first.
|
|
162
162
|
if (board !== undefined)
|
|
163
163
|
params.set("board", String(board));
|
|
164
164
|
}
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* - 50arms_api the generic surface for every non-agent-specific dashboard
|
|
15
15
|
* operation (DX-3465 / PLN-11 R-25) — list/get/create/update/delete
|
|
16
16
|
* against the uniform resource routes that used to each have their
|
|
17
|
-
* own per-operation tool (issue_*, plan_
|
|
17
|
+
* own per-operation tool (issue_*, plan_*,
|
|
18
18
|
* brief_*, repo_knowledge_*, failure_category_*,
|
|
19
19
|
* dispatch_transcript_search, quality_gate_instruction — all deleted
|
|
20
20
|
* here). See `SERVER_INSTRUCTIONS` below for the calling convention.
|
|
@@ -28,9 +28,10 @@
|
|
|
28
28
|
* with the approval URL and confirm code, and the grant is picked up in
|
|
29
29
|
* the background (`permission-request.ts`). A dashboard call refused
|
|
30
30
|
* `403 forbidden` carries a hint to call it (`forbidden-hint.ts`).
|
|
31
|
-
* -
|
|
32
|
-
* dedicated tool;
|
|
33
|
-
*
|
|
31
|
+
* - session_connect POST|DELETE /api/plan-sessions/me/connect — SESSION-BOUND (a
|
|
32
|
+
* dedicated tool; one tool for a plan and a card, named by its
|
|
33
|
+
* `target`; every plan READ or WRITE operation goes
|
|
34
|
+
* through 50arms_api, e.g. GET /api/plans/mine or
|
|
34
35
|
* POST /api/plans/mine/records)
|
|
35
36
|
*
|
|
36
37
|
* DX-2683, updated DX-3465 — PLAN WRITES ARE SESSION-BOUND, and
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
* a plan write is an ordinary `50arms_api` call to a `/api/plans/mine/...`
|
|
41
42
|
* route. Reads may name any plan by id (`/api/plans/:id/...`); a WRITE
|
|
42
43
|
* route instead takes NO plan id at all and acts on the plan this session
|
|
43
|
-
* is connected to (`
|
|
44
|
+
* is connected to (`session_connect`), resolved server-side from the session
|
|
44
45
|
* id this package forwards on every request. A session therefore cannot
|
|
45
46
|
* modify a plan it is not connected to, whatever id it believes. A write
|
|
46
47
|
* from an unconnected session is refused loudly (`session_not_connected`),
|
|
@@ -58,8 +59,8 @@
|
|
|
58
59
|
* DANX_DASHBOARD_CREDENTIAL the bearer verbatim (a dispatch, substituted into
|
|
59
60
|
* the materialized `.mcp.json` from its overlay)
|
|
60
61
|
* otherwise this session's approved key (`session-key.ts`), else none: the
|
|
61
|
-
* server boots SIGNED OUT, every tool but `
|
|
62
|
-
* halts, and `
|
|
62
|
+
* server boots SIGNED OUT, every tool but `session_connect`
|
|
63
|
+
* halts, and `session_connect` requests access (`session-access.ts`)
|
|
63
64
|
* DANXBOT_TEAM OPTIONAL (DX-4446), non-secret: the team slug (or id) whose
|
|
64
65
|
* approvers see this session's access request in their danxbot
|
|
65
66
|
* site. Set it in your shell or a repo's committed `.claude/settings.json` `env` block.
|
|
@@ -103,7 +104,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
103
104
|
import { z } from "zod";
|
|
104
105
|
import { boardRefArgSchema, parseBoardRefText, repoNameArgSchema } from "./board-id.js";
|
|
105
106
|
import { DashboardHttpClient } from "./http-client.js";
|
|
106
|
-
import {
|
|
107
|
+
import { claimSessionAgent, sessionConnect, danxbotApi, danxbotApiList, danxbotApiSpec, attachFile, DANXBOT_API_METHODS, } from "./handlers.js";
|
|
107
108
|
import { miniAppPublish } from "./mini-app-publish.js";
|
|
108
109
|
import { miniAppRestore } from "./mini-app-restore.js";
|
|
109
110
|
import { pluginPublish } from "./plugin-publish.js";
|
|
@@ -180,7 +181,7 @@ let listenerReminder;
|
|
|
180
181
|
* DX-4390 — this server's credential: a dispatch's env bearer, this session's approved key, or
|
|
181
182
|
* none (signed out). The plan event relay in this server signs in with it directly (`access.currentToken()`).
|
|
182
183
|
* `restart_notice` runs against other sessions' credentials and cannot see it, so `access.connection()` (the URL, where the credential is, its
|
|
183
|
-
* fingerprint; never the credential itself) is written to the session's connection record on a successful `
|
|
184
|
+
* fingerprint; never the credential itself) is written to the session's connection record on a successful `session_connect`
|
|
184
185
|
* for it.
|
|
185
186
|
*/
|
|
186
187
|
let access;
|
|
@@ -308,7 +309,7 @@ async function readHeldPermissions() {
|
|
|
308
309
|
/**
|
|
309
310
|
* How hard a failed permission read is for this session. A dispatch cannot work without its tools, so it retries a few times (a
|
|
310
311
|
* dashboard blip at spawn must not cost it its MCP server) and then fails the boot; an operator session logs it and keeps the
|
|
311
|
-
* bootstrap set until its next `
|
|
312
|
+
* bootstrap set until its next `session_connect` or the permission stream's next connect.
|
|
312
313
|
*/
|
|
313
314
|
function syncPolicy() {
|
|
314
315
|
const isDispatch = access.connection()?.source.kind === "env";
|
|
@@ -327,7 +328,7 @@ async function syncToolsWithPolicy() {
|
|
|
327
328
|
}
|
|
328
329
|
/**
|
|
329
330
|
* DX-4523 - show the tools the key's permissions allow, and watch for a grant. Called at boot and again whenever a
|
|
330
|
-
* `
|
|
331
|
+
* `session_connect` finds the session signed in, which is the moment a first approval or a re-approved key takes effect.
|
|
331
332
|
*/
|
|
332
333
|
async function syncToolsAfterSignIn() {
|
|
333
334
|
if (!access.isSignedIn())
|
|
@@ -336,7 +337,7 @@ async function syncToolsAfterSignIn() {
|
|
|
336
337
|
permissionWatch.start();
|
|
337
338
|
}
|
|
338
339
|
/**
|
|
339
|
-
* DX-6157 - a person created the agent this session proposed, and the dashboard signed the session in as it (and connected it to its plan or card) without a `
|
|
340
|
+
* DX-6157 - a person created the agent this session proposed, and the dashboard signed the session in as it (and connected it to its plan or card) without a `session_connect` from the session. The
|
|
340
341
|
* session therefore never saw a connect answer, so this does here what one that binds does: it opens the agent gate (`50arms_api` stops answering `agent_needed`), shows the tools the key's
|
|
341
342
|
* permissions allow, restarts the listener reminder's health read and, for a session now on a plan or card, writes the local connection record `restart_notice` and the listener reminder read.
|
|
342
343
|
* Called by the relay's listener as it writes the notice; a failure is a stderr line, never silence (the notice itself still reaches the session).
|
|
@@ -346,7 +347,7 @@ function adoptCreatedAgent(created) {
|
|
|
346
347
|
listenerReminder.invalidate();
|
|
347
348
|
const connection = access.connection();
|
|
348
349
|
if (created.plan !== null || created.card !== null) {
|
|
349
|
-
// DX-6157: the frame arrived over this key's own stream, so no credential here is a broken invariant: said loudly, as
|
|
350
|
+
// DX-6157: the frame arrived over this key's own stream, so no credential here is a broken invariant: said loudly, as session_connect throws on it.
|
|
350
351
|
if (connection === null) {
|
|
351
352
|
console.error("agent created: no credential after the dashboard signed this session in; the local connection record was not written");
|
|
352
353
|
}
|
|
@@ -405,8 +406,8 @@ function resolveInitialCredential(dashboardUrl, sessionId) {
|
|
|
405
406
|
* MCP-server-reachable signal carries Claude's own session title — this
|
|
406
407
|
* SERVER genuinely cannot read it. The AGENT can, though (a Desktop-side
|
|
407
408
|
* `get_session` tool this server has no access to), and the agent is what
|
|
408
|
-
* calls `
|
|
409
|
-
* argument (see `handlers.ts#
|
|
409
|
+
* calls `session_connect` — so the title travels as that tool's own `title`
|
|
410
|
+
* argument (see `handlers.ts#sessionConnect`) at connect time, not as a header
|
|
410
411
|
* stamped here. DX-4461: the session's TRANSCRIPT carries every rename as a
|
|
411
412
|
* `custom-title` record, so the event relay in this server reads it from
|
|
412
413
|
* there and reports it on its heartbeat (`session-title.ts`) and a later rename needs no
|
|
@@ -420,7 +421,7 @@ function readSessionConfig() {
|
|
|
420
421
|
return undefined;
|
|
421
422
|
return { id };
|
|
422
423
|
}
|
|
423
|
-
// DX-5549: whether this session holds a team agent; closed by a
|
|
424
|
+
// DX-5549: whether this session holds a team agent; closed by a session_connect answer of `agent_needed` / `agent_limit_reached`, opened only by one carrying an agent.
|
|
424
425
|
const agentGate = new AgentGate();
|
|
425
426
|
/**
|
|
426
427
|
* DX-3465 (PLN-11 R-25) — the conventions shared by every `50arms_api` call,
|
|
@@ -473,7 +474,7 @@ function connectedDashboard() {
|
|
|
473
474
|
return dashboardIdentity(config.baseUrl);
|
|
474
475
|
}
|
|
475
476
|
/**
|
|
476
|
-
* DX-4390 — `
|
|
477
|
+
* DX-4390 — `session_connect`'s answer while access is not in place: the request (or why there is none),
|
|
477
478
|
* an error when it names a failure. `note` says why a new request was needed.
|
|
478
479
|
*/
|
|
479
480
|
function accessReply(ready, note) {
|
|
@@ -862,52 +863,50 @@ strictTool("request_permission", "Ask a person to grant this key ONE MORE permis
|
|
|
862
863
|
}
|
|
863
864
|
return jsonResult(asked.answer);
|
|
864
865
|
});
|
|
865
|
-
// ----------------
|
|
866
|
+
// ---------------- session connect (DX-2683, DX-5553, DX-6917) ----------------
|
|
866
867
|
// THE BINDING ASYMMETRY IS IN THE SCHEMAS, NOT IN PROSE. `GET /api/plans/:planId`
|
|
867
868
|
// takes a plan id in its path; the plan records, plan cards and
|
|
868
869
|
// architecture-section routes under `/api/plans/mine/...` take NO plan id in
|
|
869
870
|
// any form, so a connected session cannot even express "write to that other
|
|
870
|
-
// plan". `
|
|
871
|
-
// is the one operation that is ABOUT a plan id
|
|
871
|
+
// plan". `session_connect` takes a target (`PLAN-n`, or a card's id) because binding a session
|
|
872
|
+
// is the one operation that is ABOUT a plan or card id, and it can only ever bind
|
|
872
873
|
// the caller's own session. `POST /api/plans` also takes no plan id, but for
|
|
873
874
|
// a different reason: it MAKES a plan rather than acting on one, so there is
|
|
874
875
|
// no existing plan for an id to name yet.
|
|
875
|
-
strictTool("
|
|
876
|
+
strictTool("session_connect",
|
|
877
|
+
// DX-6917 - the one connect tool: what used to be `plan_connect` (a plan) and `card_connect` (DX-5553, a card on no plan). The dashboard resolves `target`; this package never parses it.
|
|
876
878
|
// DX-4493 — the `naming` block carries the thread title the session must take before it connects.
|
|
877
879
|
// DX-4318 — states only what to do with the reply. What it used to add arrives where it is used: a rejected
|
|
878
880
|
// action's rule in the event line itself (`listen.ts`), the mantra in the reply's own `reminders[]` text.
|
|
879
881
|
// DX-4390 — signed out, this is also how the session asks for access.
|
|
880
|
-
|
|
882
|
+
// DX-4608 / DX-5549: a title in the format naming another agent than the held one is refused.
|
|
883
|
+
"Connect THIS session to ONE plan or card, named by `target`: `PLAN-n`, or a card's id for a card on no plan. One call is enough to start; omit `target` only to sign in (it still takes your team agent). Signed out, it requests access: " +
|
|
881
884
|
"the reply is `state: approval_required` or `approval_pending` with an `approvalUrl` and `confirmCode` " +
|
|
882
|
-
"(show the code, open the URL, then call again;
|
|
883
|
-
"Signed in, the reply carries `owedComments` (person comments no agent has answered yet: read first, answer each, ack then reply)
|
|
884
|
-
"events no session had been told about: read next), " +
|
|
885
|
-
"`browserInstruction` (do it now), `briefing` (goals, rules, caveats, architecture " +
|
|
886
|
-
"
|
|
887
|
-
"
|
|
888
|
-
"
|
|
889
|
-
"
|
|
890
|
-
"
|
|
891
|
-
"
|
|
892
|
-
"no other dashboard work until `plan_connect` holds an agent. A `naming` block (status `needed`) gives the exact " +
|
|
893
|
-
"`title`, `{Name}: PLAN-NNN {short title}`; rename this thread to it (`set_session_title`, or ask for `/rename`), then connect again with that `title`. " +
|
|
894
|
-
// DX-4608 / DX-5549: a title in the format naming another agent than the held one is refused.
|
|
885
|
+
"(show the code, open the URL, then call again; a call re-asks an expired one). " +
|
|
886
|
+
"Signed in, the reply carries `owedComments` (person comments no agent has answered yet: read first, answer each, ack then reply), `missedEvents` (operator " +
|
|
887
|
+
"events no session had been told about: read next), `naming` and `listenerHealth` (when its `state` is not `healthy`, do its `nextStep`); " +
|
|
888
|
+
"for a plan also `browserInstruction` (do it now), `briefing` (goals, rules, caveats, architecture, open cards) and `docsLandJob` (a docs land assignment's job). " +
|
|
889
|
+
"A session holds a plan or a card, never both: connecting elsewhere moves it (`movedFrom` names the plan it left). " +
|
|
890
|
+
"A plan holds one live session: another on it refuses the connect (`plan_has_live_session`); do not retry. " +
|
|
891
|
+
"A card is refused `card_on_plan` when it or a card above it is on a plan (connect to that plan) and `card_held` when another live session holds it; holding one assigns it to you, like a manual pickup. " +
|
|
892
|
+
"Afterwards `50arms_api` calls to `/api/plans/mine/...` act on your plan, no plan id. Connecting signs you in as a team agent; with none to sign in as: `agent_needed` or `agent_limit_reached` (the cap is full: ask the person), nothing connects: follow its `instruction`, " +
|
|
893
|
+
"no other dashboard work until `session_connect` holds an agent. A `naming` block (status `needed`) gives the exact " +
|
|
894
|
+
"`title`, `{Name}: PLAN-NNN {short title}` or `{Name}: {CARD-ID} {card title}`; rename this thread to it (`set_session_title`, or ask for `/rename`), then connect again with that `title`. " +
|
|
895
895
|
"A `title` in that format naming another agent than yours is refused `session_title_not_agent_name` with the title to use.", {
|
|
896
|
-
|
|
897
|
-
.
|
|
898
|
-
.
|
|
899
|
-
.positive()
|
|
896
|
+
target: z
|
|
897
|
+
.string()
|
|
898
|
+
.min(1)
|
|
900
899
|
.optional()
|
|
901
|
-
.describe("
|
|
900
|
+
.describe("What to hold: `PLAN-n` (a plan's `ref` in `50arms_api` `GET /api/plans`) or a card's id as the board shows it, for a card on no plan. Omit only to sign in (it still takes your team agent)."),
|
|
902
901
|
title: z
|
|
903
902
|
.string()
|
|
904
903
|
.min(1)
|
|
905
904
|
.optional()
|
|
906
905
|
.describe(
|
|
907
906
|
// DX-4608: stored as sent; a wrong one does not refuse the connect, the reply's `naming` says what to rename it to.
|
|
908
|
-
'The `naming` block\'s exact `{Name}: PLAN-NNN {short title}`, after renaming this thread. Omit on the first connect.'),
|
|
907
|
+
'The `naming` block\'s exact `{Name}: PLAN-NNN {short title}` or `{Name}: {CARD-ID} {card title}`, after renaming this thread. Omit on the first connect.'),
|
|
909
908
|
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
|
-
disconnect: z.boolean().optional().describe("true = leave `
|
|
909
|
+
disconnect: z.boolean().optional().describe("true = leave `target` (the plan or card you hold); no `title`."),
|
|
911
910
|
launch_token: z
|
|
912
911
|
.string()
|
|
913
912
|
.min(1)
|
|
@@ -921,21 +920,21 @@ strictTool("plan_connect",
|
|
|
921
920
|
"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."),
|
|
922
921
|
},
|
|
923
922
|
// DX-2862 — the connect is also the moment the session's connection record is written for `restart_notice` and the listener reminder: of the
|
|
924
|
-
// dashboard MCP servers a session may have configured, the one that connected the plan is the one whose credential they
|
|
923
|
+
// dashboard MCP servers a session may have configured, the one that connected the plan or card is the one whose credential they
|
|
925
924
|
// must resolve (the relay needs none of this: it lives in each server, with that server's own credential).
|
|
926
925
|
async (args) => {
|
|
927
926
|
// DX-6894 — a person's Sign in press leaves the revoked stop; every call without it still answers that stop.
|
|
928
927
|
if (args.person_sign_in === true)
|
|
929
928
|
access.personSignIn();
|
|
930
|
-
// DX-4390 — a leave names
|
|
931
|
-
if (args.disconnect === true && args.
|
|
932
|
-
return haltResult("
|
|
929
|
+
// DX-4390 — a leave names what it leaves, and needs a key like any other call.
|
|
930
|
+
if (args.disconnect === true && args.target === undefined) {
|
|
931
|
+
return haltResult("session_connect: `disconnect: true` needs `target`, the plan or card this session currently holds");
|
|
933
932
|
}
|
|
934
933
|
// DX-4390 — no key yet: ask for one (or wait for the approval already asked for).
|
|
935
934
|
const ready = await access.ensureAccess(args.title, args.launch_token);
|
|
936
935
|
if (!ready.signedIn)
|
|
937
936
|
return accessReply(ready);
|
|
938
|
-
if (args.
|
|
937
|
+
if (args.target === undefined) {
|
|
939
938
|
// DX-4523 - signed in (possibly just now): show the tools this key's permissions allow.
|
|
940
939
|
await syncToolsAfterSignIn();
|
|
941
940
|
// DX-5549: signing in takes a team agent. The dashboard's answer (the agent and the instruction to use its name, or why there is none) is the reply.
|
|
@@ -950,11 +949,11 @@ async (args) => {
|
|
|
950
949
|
void reportCheckoutDocsRoot();
|
|
951
950
|
return jsonResult({
|
|
952
951
|
...claimed.body,
|
|
953
|
-
...(agentGate.current() === null ? { instruction: "This session is signed in. Call `
|
|
952
|
+
...(agentGate.current() === null ? { instruction: "This session is signed in. Call `session_connect` with `target` to connect it to a plan or a card." } : {}),
|
|
954
953
|
});
|
|
955
954
|
}
|
|
956
955
|
// DX-6089: the role text lands in a local file; the reply names it (`role-instruction-file.ts`).
|
|
957
|
-
const result = spillRoleInstruction(await
|
|
956
|
+
const result = spillRoleInstruction(await sessionConnect(client, { ...args, target: args.target }));
|
|
958
957
|
// DX-4418 — a person revoked this key: the stop halt, and nothing asks again.
|
|
959
958
|
const revokedHalt = access.refusalHalt(result);
|
|
960
959
|
if (revokedHalt !== null)
|
|
@@ -963,12 +962,12 @@ async (args) => {
|
|
|
963
962
|
if (result.status === 401 && access.dropKey(result)) {
|
|
964
963
|
return accessReply(await access.ensureAccess(args.title), "the dashboard no longer accepted this session's key");
|
|
965
964
|
}
|
|
966
|
-
return finishConnect("
|
|
965
|
+
return finishConnect("session_connect", result, args.disconnect === true);
|
|
967
966
|
});
|
|
968
967
|
// DX-6735: started by each connect that holds an agent, and never awaited by it.
|
|
969
968
|
const reportCheckoutDocsRoot = createCheckoutDocsRootReporter(() => client, process.cwd());
|
|
970
969
|
/**
|
|
971
|
-
* What `
|
|
970
|
+
* What `session_connect` does with a connect answer once the key is known good (DX-6157 - one path for a plan and a card), and `50arms_api` with a manual pickup that connected (DX-6556).
|
|
972
971
|
* DX-5549: the dashboard connects only a session that holds a team agent. With none it answers why (`agent_needed` / `agent_limit_reached`) and binds nothing: that answer is handed to the agent
|
|
973
972
|
* unchanged and every other dashboard tool stays closed until a later connect finds an agent. The tools are shown and the listener reminder reset BEFORE that block is handed back (DX-4523, DX-3997),
|
|
974
973
|
* so a session whose key was just approved can run `plan_events_wait` and hear the person's Create. A leave (DX-2985) drops the local connection record; a connect writes it.
|
|
@@ -1001,43 +1000,6 @@ async function finishConnect(tool, result, disconnect) {
|
|
|
1001
1000
|
}),
|
|
1002
1001
|
});
|
|
1003
1002
|
}
|
|
1004
|
-
// ---------------- card_connect (DX-5553) ----------------
|
|
1005
|
-
strictTool("card_connect",
|
|
1006
|
-
// DX-5553 - the stand-alone counterpart of `plan_connect`: one card on no plan instead of a plan.
|
|
1007
|
-
"Hold ONE card that is on no plan instead of a plan: for a one-off, a quick fix or an epic that is not a plan. You then hear that card's and its descendants' events, " +
|
|
1008
|
-
"and no other card's. The reply carries `owedComments` (every person comment on it no agent has answered yet, oldest first: read them first and answer each, ack then reply), `missedEvents` (operator events no session had been told about on it: read them next), `naming` and `agent`/`agentInstruction`. " +
|
|
1009
|
-
"Refused `card_on_plan` when the card or any card above it is on a plan (connect to that plan with `plan_connect`), `card_held` when another live session holds it. " +
|
|
1010
|
-
"A session holding no team agent is first signed in as one, as `plan_connect` does: with none free the answer is `agent_needed` (propose one) or `agent_limit_reached`, and nothing is held. " +
|
|
1011
|
-
"Holding it assigns the card to you, like a manual pickup; a card " +
|
|
1012
|
-
"with children is held as a whole and its children are picked up normally. A session holds a plan or a card, never both: connecting to either lets go of the other. " +
|
|
1013
|
-
"A `naming` block (status `needed`) gives the exact `title`, `{Name}: {CARD-ID} {card title}`; rename this thread to it (`set_session_title`, or ask for `/rename`), then connect " +
|
|
1014
|
-
"again with that `title`. A card later added to a plan moves you off it: you are told on your next plan call.", {
|
|
1015
|
-
card_id: z.string().min(1).describe("The id of the card to hold, as the board shows it."),
|
|
1016
|
-
title: z
|
|
1017
|
-
.string()
|
|
1018
|
-
.min(1)
|
|
1019
|
-
.optional()
|
|
1020
|
-
.describe("The `naming` block's exact `{Name}: {CARD-ID} {card title}`, after renaming this thread. Omit on the first connect."),
|
|
1021
|
-
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."),
|
|
1022
|
-
disconnect: z.boolean().optional().describe("true = let go of `card_id` (the card you hold); no `title`."),
|
|
1023
|
-
launch_token: z
|
|
1024
|
-
.string()
|
|
1025
|
-
.min(1)
|
|
1026
|
-
.optional()
|
|
1027
|
-
.describe("The one-use token the session that started this one passed in its prompt. With it a signed-out session signs in with no person approving it; a token that cannot be used says why and the session asks a person as usual. Omit it otherwise."),
|
|
1028
|
-
}, async (args) => {
|
|
1029
|
-
const ready = await access.ensureAccess(args.title, args.launch_token);
|
|
1030
|
-
if (!ready.signedIn)
|
|
1031
|
-
return accessReply(ready);
|
|
1032
|
-
const result = spillRoleInstruction(await cardConnect(client, args));
|
|
1033
|
-
const revokedHalt = access.refusalHalt(result);
|
|
1034
|
-
if (revokedHalt !== null)
|
|
1035
|
-
return haltResult(revokedHalt);
|
|
1036
|
-
if (result.status === 401 && access.dropKey(result)) {
|
|
1037
|
-
return accessReply(await access.ensureAccess(args.title), "the dashboard no longer accepted this session's key");
|
|
1038
|
-
}
|
|
1039
|
-
return finishConnect("card_connect", result, args.disconnect === true);
|
|
1040
|
-
});
|
|
1041
1003
|
// ---------------- plan_events_wait (DX-4233) ----------------
|
|
1042
1004
|
strictTool("plan_events_wait",
|
|
1043
1005
|
// DX-4882 - `plan_id` is null for a signed-in session on no plan: its stream still carries its account's pacing and resume messages.
|
package/dist/key-revoked-halt.js
CHANGED
|
@@ -26,5 +26,5 @@ export function keyRevokedHalt(revocation, dispatched) {
|
|
|
26
26
|
return (`STOP ALL WORK NOW. ${revocation.revokedBy} revoked your access to the 50 Arms dashboard at ${revocation.revokedAt}.\n\n` +
|
|
27
27
|
"Commit your work in progress now (commit what you have; do not run agent-finalize.sh and do not merge). " +
|
|
28
28
|
`${ending}\n\n` +
|
|
29
|
-
"Do not call
|
|
29
|
+
"Do not call session_connect or any other 50 Arms dashboard tool again, do not request access again and do not look for another route.");
|
|
30
30
|
}
|
package/dist/listen.js
CHANGED
|
@@ -613,7 +613,7 @@ export function formatAgentCreatedLine(event) {
|
|
|
613
613
|
const held = event.plan !== null ? "this agent and plan" : event.card !== null ? "this agent and card" : "this agent";
|
|
614
614
|
const ownCalls = thread !== "" || event.rolePointer !== null ? "; the steps below keep their own calls" : "";
|
|
615
615
|
const parts = [
|
|
616
|
-
`${LINE_PREFIX} The person created your team agent ${oneLine(event.agent.name, 60)}: this session is now signed in as it${where}. Taking ${held} needs no \`
|
|
616
|
+
`${LINE_PREFIX} The person created your team agent ${oneLine(event.agent.name, 60)}: this session is now signed in as it${where}. Taking ${held} needs no \`session_connect\`${ownCalls}.`,
|
|
617
617
|
oneLine(event.agentInstruction, 800),
|
|
618
618
|
thread === "" ? "" : oneLine(thread, 800),
|
|
619
619
|
event.owedComments === null ? "" : oneLine(event.owedComments),
|
package/dist/machine-secret.js
CHANGED
|
@@ -27,7 +27,7 @@ export function machineSecretPath(dashboardUrl, home = homedir()) {
|
|
|
27
27
|
return join(home, ".config", "danxbot", "machine-secrets", `${dashboardHash(dashboardUrl)}.json`);
|
|
28
28
|
}
|
|
29
29
|
function unusable(path, why) {
|
|
30
|
-
return new Error(`[platform-mcp] the machine secret file ${path} is unusable: ${why}; fix or delete it, then call
|
|
30
|
+
return new Error(`[platform-mcp] the machine secret file ${path} is unusable: ${why}; fix or delete it, then call session_connect again`);
|
|
31
31
|
}
|
|
32
32
|
/** The secret in `path`, or null when the file is absent. A file that is there but of another shape throws. */
|
|
33
33
|
function readSecret(path, dashboardUrl) {
|
package/dist/machine-toolkit.js
CHANGED
|
@@ -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.
|
|
253
|
+
var TOOLKIT_VERSION = resolveToolkitVersion(false ? void 0 : "0.1.444", 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
|
}
|
package/dist/permission-watch.js
CHANGED
|
@@ -90,7 +90,7 @@ export class PermissionWatch {
|
|
|
90
90
|
});
|
|
91
91
|
if (response.status >= 400 && response.status < 500 && !RETRYABLE_REFUSALS.has(response.status)) {
|
|
92
92
|
this.deps.log?.(`[platform-mcp] the dashboard refused the permission stream (HTTP ${response.status}): a permission granted ` +
|
|
93
|
-
`mid-session shows its tools at the next
|
|
93
|
+
`mid-session shows its tools at the next session_connect instead`);
|
|
94
94
|
await response.body?.cancel();
|
|
95
95
|
return { kind: "refused" };
|
|
96
96
|
}
|
package/dist/restart-notice.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* WHY IT EXISTS. A restarted session gets a NEW session id, no plan binding and, until a person approves its first call, no
|
|
6
6
|
* dashboard key of its own: its MCP server boots signed out. Nothing told it (or the operator) that the plan it had been
|
|
7
7
|
* working was unattended: six operator answers sat unread for an hour on 2026-10-01. The session's own connection record cannot
|
|
8
|
-
* help (it does not exist yet); the PREVIOUS session's record can, found by the project key `
|
|
8
|
+
* help (it does not exist yet); the PREVIOUS session's record can, found by the project key `session_connect` wrote into it
|
|
9
9
|
* (`findPredecessorCandidates`). So this asks the dashboard about each predecessor as the predecessor, with the credential
|
|
10
10
|
* that predecessor's record names (`resolveDashboardAuth`, the resolver `session-options.ts` holds), which a signed-out
|
|
11
|
-
* session needs no key of its own to do. The tool is therefore usable signed out: it is declared like `
|
|
11
|
+
* session needs no key of its own to do. The tool is therefore usable signed out: it is declared like `session_connect` (no
|
|
12
12
|
* permission) and only on a person's session (`tool-access.ts`).
|
|
13
13
|
*
|
|
14
14
|
* THE ANSWER (the plugin's hooks module reads it through `$.mcp.call`):
|
package/dist/result-limit.js
CHANGED
|
@@ -6,7 +6,7 @@ import { writeFileDurable } from "./durable-write.js";
|
|
|
6
6
|
* DX-4400 — the most characters one `50arms_api`, `50arms_api_spec` or `50arms_api_list` result may carry
|
|
7
7
|
* back to the agent. Every character is context the agent pays for on every later call, and Claude Code itself
|
|
8
8
|
* saves a larger result to a file with only a short preview, after the cost is paid. A deliberate, narrow
|
|
9
|
-
* exception to PLAN-11 rule R-25 (verbatim forwarding): `
|
|
9
|
+
* exception to PLAN-11 rule R-25 (verbatim forwarding): `session_connect` and the lifecycle tools never pass here.
|
|
10
10
|
*/
|
|
11
11
|
export const MCP_MAX_RESULT_CHARS = 100_000;
|
|
12
12
|
const FILE_INSTRUCTION = "The reply was over the inline limit, so its body is this file, whole: read it with Read (in parts with offset and limit when it is long), or grep it.";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DX-6089 - a session's role text reaches it as a local file, never inline. The dashboard answers `roleInstruction: {profile, text}` on
|
|
3
|
-
* `
|
|
3
|
+
* `session_connect` and `GET /api/plan-sessions/me/role`; the text runs to tens of KB, and a tool reply that large is
|
|
4
4
|
* saved to a file by the client with only a short preview, which also hides every field after it (the briefing, the agent instruction, the
|
|
5
5
|
* reminders). So this server writes the text to `<tmp>/danx-role/<profile>-<content hash>.md` and answers `{profile, path, bytes, instruction}`
|
|
6
6
|
* in its place, leaving every other field inline, as `docs_read` does for a docs page (`docs-tools.ts`). The name carries the content hash, so a
|
package/dist/session-access.js
CHANGED
|
@@ -6,17 +6,17 @@
|
|
|
6
6
|
* - `env` a dispatch declared `DANX_DASHBOARD_CREDENTIAL`; this never signs out.
|
|
7
7
|
* - `session-key` a user approved this session's access request; the key is stored per session
|
|
8
8
|
* (`session-key.ts`) and a 401 on it deletes the file (`dropKey`).
|
|
9
|
-
* - signed out no credential. Every tool but `
|
|
9
|
+
* - signed out no credential. Every tool but `session_connect` halts; `session_connect` asks.
|
|
10
10
|
*
|
|
11
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 `
|
|
12
|
+
* (as opposed to one that lapsed); this object then remembers it, every dashboard tool (and `session_connect`) returns the one stop
|
|
13
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
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 (`
|
|
15
|
+
* makes (`session_connect` with `person_sign_in: true`); nothing an agent calls lifts the stop.
|
|
16
16
|
*
|
|
17
|
-
* ASKING NEVER BLOCKS FOR MINUTES. The first signed-out `
|
|
17
|
+
* ASKING NEVER BLOCKS FOR MINUTES. The first signed-out `session_connect` posts the request and
|
|
18
18
|
* answers at once with the approval URL and confirm code. From then on THIS object polls claim in
|
|
19
|
-
* the background, so the key is stored the moment the user approves, and the next `
|
|
19
|
+
* the background, so the key is stored the moment the user approves, and the next `session_connect`
|
|
20
20
|
* waits at most `awaitMs` (about 45s) for it before answering `approval_pending`. The secret the
|
|
21
21
|
* poll needs lives only in this process's memory; a restarted session asks again.
|
|
22
22
|
*
|
|
@@ -32,7 +32,7 @@ import { keyRevocationOf, keyRevokedHalt } from "./key-revoked-halt.js";
|
|
|
32
32
|
import { machineSecretFor } from "./machine-secret.js";
|
|
33
33
|
import { deleteSessionKey, writeSessionKey } from "./session-key.js";
|
|
34
34
|
import { McpOutdatedError } from "./version-floor.js";
|
|
35
|
-
/** Longest a `
|
|
35
|
+
/** Longest a `session_connect` call waits for an approval already asked for. */
|
|
36
36
|
export const AWAIT_APPROVAL_MS = 45_000;
|
|
37
37
|
/** A live server rewrites its key file this often, so an in-use key never ages out of the 7-day prune. */
|
|
38
38
|
const KEEP_KEY_FILE_EVERY_MS = 60 * 60 * 1000;
|
|
@@ -40,22 +40,22 @@ const KEEP_KEY_FILE_EVERY_MS = 60 * 60 * 1000;
|
|
|
40
40
|
const REFUSAL_LOG_LIMIT = 300;
|
|
41
41
|
/** The request's title when the agent gave none: the session id's first characters. */
|
|
42
42
|
const TITLE_FALLBACK_LENGTH = 8;
|
|
43
|
-
export const SIGN_IN_HALT = "Not signed in to the 50 Arms dashboard. Call `
|
|
43
|
+
export const SIGN_IN_HALT = "Not signed in to the 50 Arms dashboard. Call `session_connect` (with `target`) to request access; the user approves it in their browser, then this tool works.";
|
|
44
44
|
/** DX-4418: the session's key lapsed (a day unused) or the dashboard does not know it; a PERSON's revoke is `keyRevokedHalt` instead. */
|
|
45
45
|
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. " +
|
|
46
46
|
SIGN_IN_HALT;
|
|
47
47
|
const APPROVAL_REQUIRED_INSTRUCTION = "Show the user the confirm code and open the approval URL in their browser; they check the code " +
|
|
48
|
-
"matches and approve. Then call `
|
|
48
|
+
"matches and approve. Then call `session_connect` again: it waits up to about 45 seconds for the approval. " +
|
|
49
49
|
"The request stays open until this session ends, however long the user takes.";
|
|
50
50
|
const APPROVAL_PENDING_INSTRUCTION = "Still waiting for the user to approve; the request stays open until this session ends. Remind them of the " +
|
|
51
|
-
"confirm code and the URL, then call `
|
|
51
|
+
"confirm code and the URL, then call `session_connect` again.";
|
|
52
52
|
/** DX-6764: a machine grant approved the request; nothing for the user to open or confirm, the next call picks the sign-in up. */
|
|
53
|
-
const SIGNING_IN_INSTRUCTION = "This machine's auto-approval signs the session in; no approval link is involved. Call `
|
|
54
|
-
const AUTO_APPROVED_EXPIRED_ERROR = "the dashboard approved this request by itself, but it expired before this process claimed the key; call
|
|
55
|
-
const DENIED_INSTRUCTION = "The user denied the request. Do not retry unless they ask; a new `
|
|
53
|
+
const SIGNING_IN_INSTRUCTION = "This machine's auto-approval signs the session in; no approval link is involved. Call `session_connect` again in a moment.";
|
|
54
|
+
const AUTO_APPROVED_EXPIRED_ERROR = "the dashboard approved this request by itself, but it expired before this process claimed the key; call session_connect again";
|
|
55
|
+
const DENIED_INSTRUCTION = "The user denied the request. Do not retry unless they ask; a new `session_connect` makes a new request.";
|
|
56
56
|
const DASHBOARD_OUTDATED_ERROR = "this dashboard does not support request-and-approve yet (POST /api/access-requests answered 404): " +
|
|
57
57
|
"it must be updated before this session can sign in";
|
|
58
|
-
const RATE_LIMITED_ERROR = "too many access requests from this address; wait, then call
|
|
58
|
+
const RATE_LIMITED_ERROR = "too many access requests from this address; wait, then call session_connect again";
|
|
59
59
|
const unknownTeamError = (team) => `the dashboard has no team "${team}" (DANXBOT_TEAM); fix DANXBOT_TEAM in this server's env (your shell, or a repo's committed .claude/settings.json env block) and restart the session`;
|
|
60
60
|
const NO_SESSION_ID_ERROR = "this server has no CLAUDE_CODE_SESSION_ID, so it cannot request a session key";
|
|
61
61
|
const CLAIMED_LOST_ERROR = "the dashboard says this request was already claimed, but this process never received the key; " +
|
|
@@ -105,7 +105,7 @@ export class SessionAccess {
|
|
|
105
105
|
// 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.
|
|
106
106
|
revoked = null;
|
|
107
107
|
pending = null;
|
|
108
|
-
// DX-4390: two concurrent signed-out
|
|
108
|
+
// DX-4390: two concurrent signed-out session_connect calls must not each post a request.
|
|
109
109
|
ensureQueue = Promise.resolve();
|
|
110
110
|
lastKeyFileWrite = 0;
|
|
111
111
|
// DX-6801: a launch token is redeemed at most once per process, whatever the answer.
|
|
@@ -235,7 +235,7 @@ export class SessionAccess {
|
|
|
235
235
|
}
|
|
236
236
|
/**
|
|
237
237
|
* Make sure this session holds a key. Signed in: `{signedIn: true}`. Otherwise the answer
|
|
238
|
-
* `
|
|
238
|
+
* `session_connect` returns: the request just made, the one still pending (after waiting up to
|
|
239
239
|
* `awaitMs` for it), or why there is none. Calls run one at a time.
|
|
240
240
|
*
|
|
241
241
|
* DX-6801 / DEC-522: a signed-out session given the `launchToken` the session that started it passed in its prompt redeems it
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* dropped silently (the card's incident). The plan event relay (`plan_events_wait`) needs none of this: it runs inside the
|
|
9
9
|
* server, with the server's own credential. The listener reminder reads this record too.
|
|
10
10
|
*
|
|
11
|
-
* WHAT IS WRITTEN, AND WHAT IS NOT. On a successful `
|
|
11
|
+
* WHAT IS WRITTEN, AND WHAT IS NOT. On a successful `session_connect` the server
|
|
12
12
|
* writes the dashboard URL, the credential SOURCE (a session key file path, or the name of
|
|
13
13
|
* an env var — never a credential) and the credential's FINGERPRINT. A reader
|
|
14
14
|
* (`restart_notice`) reads that record, resolves the SAME source through the same resolver
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* identically. A rotated file or a session environment that disagrees with the
|
|
17
17
|
* server's therefore fails loud instead of reporting as somebody else.
|
|
18
18
|
*
|
|
19
|
-
* WHY `
|
|
19
|
+
* WHY `session_connect` AND NOTHING ELSE WRITES IT. A session can have several
|
|
20
20
|
* dashboard MCP servers configured (gpt-manager has a deployed one and a local
|
|
21
21
|
* one). The server that CONNECTED the session to a plan is, by construction,
|
|
22
22
|
* the one whose credential owns that session's stream — so the connect is the
|
|
@@ -143,7 +143,7 @@ export function readSessionConnection(sessionId, options = {}) {
|
|
|
143
143
|
return parsed;
|
|
144
144
|
}
|
|
145
145
|
/**
|
|
146
|
-
* Record the connection a successful `
|
|
146
|
+
* Record the connection a successful `session_connect` just made, and say so in
|
|
147
147
|
* that tool's own answer when it could not be recorded.
|
|
148
148
|
*
|
|
149
149
|
* FAIL LOUD, IN THE ONE PLACE THE AGENT IS LOOKING. A connect whose record is
|
|
@@ -179,14 +179,14 @@ export function recordSessionConnectionAfterConnect(result, inputs, write = writ
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
/**
|
|
182
|
-
* DX-6556 — a manual pickup, which goes through the generic `50arms_api`, can connect the session as
|
|
182
|
+
* DX-6556 — a manual pickup, which goes through the generic `50arms_api`, can connect the session as a card connect does; the dashboard marks that answer, and it is then
|
|
183
183
|
* handled as a connect, so no record or tool sync is missed.
|
|
184
184
|
*/
|
|
185
185
|
export function isPickupConnect(result) {
|
|
186
186
|
return result.ok && typeof result.body === "object" && result.body !== null && result.body.connectedByPickup === true;
|
|
187
187
|
}
|
|
188
188
|
/**
|
|
189
|
-
* DX-2985 — drop this session's local connection record after a successful leave (`
|
|
189
|
+
* DX-2985 — drop this session's local connection record after a successful leave (`session_connect {disconnect: true}`), so
|
|
190
190
|
* nothing on this machine still treats the session as plan-connected (the listener-reminder reads this record). A refused
|
|
191
191
|
* leave keeps it: the session is still on its plan. Never throws for an absent record.
|
|
192
192
|
*/
|
|
@@ -202,7 +202,7 @@ export function invalidRecordReason(value) {
|
|
|
202
202
|
const r = value;
|
|
203
203
|
if (r.schemaVersion !== SESSION_CONNECTION_SCHEMA_VERSION) {
|
|
204
204
|
return (`schemaVersion is ${JSON.stringify(r.schemaVersion)}, not ${SESSION_CONNECTION_SCHEMA_VERSION} — ` +
|
|
205
|
-
"a session connected by an older MCP server wrote it; call
|
|
205
|
+
"a session connected by an older MCP server wrote it; call session_connect again to rewrite it");
|
|
206
206
|
}
|
|
207
207
|
for (const field of ["sessionId", "projectKey", "dashboardUrl", "fingerprint", "connectedAt"]) {
|
|
208
208
|
if (typeof r[field] !== "string" || r[field] === "")
|
package/dist/session-title.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* DX-4461 - a session's live Claude title, read from its own transcript with no help from the agent.
|
|
3
3
|
*
|
|
4
4
|
* WHY THE TRANSCRIPT. The dashboard stores a plan session's title, and until now it arrived only as the `title`
|
|
5
|
-
* argument of `
|
|
5
|
+
* argument of `session_connect`, so a session renamed afterwards showed a stale name until the agent happened to connect
|
|
6
6
|
* again. DX-2816 found no env var, hook input or MCP-reachable signal carrying the title, but Claude Code writes every
|
|
7
7
|
* title change into the session's transcript as a `{"type":"custom-title","customTitle":"..."}` line the moment the
|
|
8
8
|
* rename happens, even while the session is idle (observed on session 952f0739: 76 records of the old title, then the
|
package/dist/stop-fixes.js
CHANGED
|
@@ -13,26 +13,26 @@
|
|
|
13
13
|
* is no separate process to restart.
|
|
14
14
|
*/
|
|
15
15
|
const RESTART_NOTICE_STOP_LEAD = "continue without it";
|
|
16
|
-
const RESTART_NOTICE_STOP_TAIL = "call
|
|
16
|
+
const RESTART_NOTICE_STOP_TAIL = "call session_connect to connect this session to a plan or a card";
|
|
17
17
|
export const STOP_FIXES = {
|
|
18
|
-
no_connection_record: "call
|
|
18
|
+
no_connection_record: "call session_connect again in this session — its platform MCP server records the connection that restart_notice and the listener reminder read " +
|
|
19
19
|
"(a server older than the one that introduced the record writes none)",
|
|
20
|
-
credential_unavailable: "call
|
|
21
|
-
credential_mismatch: "restart this Claude Code session so its MCP server resolves its credential afresh, then call
|
|
22
|
-
not_connected: "call
|
|
23
|
-
plan_archived: "the plan was archived or deleted, so this session was disconnected; ask the operator to restore it, then call
|
|
24
|
-
unauthorized: "the dashboard no longer accepts this session's key (it lapsed after a day unused): call
|
|
20
|
+
credential_unavailable: "call session_connect again to request access: this session's key file is missing or unreadable",
|
|
21
|
+
credential_mismatch: "restart this Claude Code session so its MCP server resolves its credential afresh, then call session_connect again",
|
|
22
|
+
not_connected: "call session_connect to connect this session to a plan or a card",
|
|
23
|
+
plan_archived: "the plan was archived or deleted, so this session was disconnected; ask the operator to restore it, then call session_connect again",
|
|
24
|
+
unauthorized: "the dashboard no longer accepts this session's key (it lapsed after a day unused): call session_connect again to request access",
|
|
25
25
|
key_revoked: "stop all work now, commit your work in progress and end this session: the person who owns this session's access revoked it; " +
|
|
26
|
-
"do not call
|
|
27
|
-
mint_refused: "check that this session is signed in to the dashboard its MCP server points at, then call
|
|
26
|
+
"do not call session_connect or request access again",
|
|
27
|
+
mint_refused: "check that this session is signed in to the dashboard its MCP server points at, then call session_connect again",
|
|
28
28
|
mint_bad_response: "check that DANXBOT_DASHBOARD_URL for this session's MCP server points at a 50 Arms dashboard",
|
|
29
29
|
// A newer ticket exists (superseded) or another connection is already using this one (replaced): THIS listener is ending
|
|
30
30
|
// because the session is already served elsewhere, not because anything is broken.
|
|
31
31
|
superseded: "no action needed — a newer ticket was minted for this session, and a fresh listener is already using it",
|
|
32
|
-
replaced: "no action needed — another connection is already using this session's ticket; call
|
|
33
|
-
revoked: "call
|
|
34
|
-
refused: "call
|
|
35
|
-
// DX-6272: calling
|
|
32
|
+
replaced: "no action needed — another connection is already using this session's ticket; call session_connect again only if that was unexpected",
|
|
33
|
+
revoked: "call session_connect again to mint a new listener ticket for this session",
|
|
34
|
+
refused: "call session_connect again to mint a new listener ticket for this session",
|
|
35
|
+
// DX-6272: calling session_connect again would reconnect with the same old relay and be refused again.
|
|
36
36
|
mcp_outdated: "restart this Claude Code session, or reconnect its platform server in /mcp, to install the current version of the MCP server; the dashboard's event stream needs a newer one than this session runs",
|
|
37
37
|
// DX-3028 - this session id belongs to a worker dispatch; there is no listener ticket a worker session can ever use.
|
|
38
38
|
session_is_worker: "this session id belongs to a worker dispatch; start a new session",
|
|
@@ -44,7 +44,7 @@ export const STOP_FIXES = {
|
|
|
44
44
|
lookup_failed: `${RESTART_NOTICE_STOP_LEAD}: no earlier session's record could be checked (the detail says why); ${RESTART_NOTICE_STOP_TAIL}`,
|
|
45
45
|
relay_failed: "the event relay inside this session's platform MCP server failed (the detail says why); the next call tries again, and if it keeps failing restart this Claude Code session",
|
|
46
46
|
};
|
|
47
|
-
export const DEFAULT_STOP_FIX = "call
|
|
47
|
+
export const DEFAULT_STOP_FIX = "call session_connect again in this session to restart the listener";
|
|
48
48
|
export function fixForStop(reason) {
|
|
49
49
|
return STOP_FIXES[reason] ?? DEFAULT_STOP_FIX;
|
|
50
50
|
}
|
package/dist/tool-access.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Three kinds of tool, by what makes them usable:
|
|
10
10
|
*
|
|
11
11
|
* - bootstrap `{permission: null, callback: null}` - what a session needs BEFORE it holds a key, or to ask for more:
|
|
12
|
-
* `
|
|
12
|
+
* `session_connect` (sign in, bind to a plan or a card), `request_permission`, and the `50arms_api` trio.
|
|
13
13
|
* - permission `{permission}` - the dashboard route the tool calls needs this permission, so a key without it can
|
|
14
14
|
* never succeed. Shown once the key holds it, which includes a grant a person approves mid-session.
|
|
15
15
|
* - callback `{callback}` - the tool posts to a per-dispatch URL the worker injected (`lifecycle.ts`), so it is usable
|
|
@@ -35,9 +35,7 @@ const needsAndPostsTo = (permission, callback) => ({ permission, callback });
|
|
|
35
35
|
* here, and `tool-access.test.ts` fails when an entry names no registered tool.
|
|
36
36
|
*/
|
|
37
37
|
export const TOOL_ACCESS = {
|
|
38
|
-
|
|
39
|
-
// DX-5553 - `POST|DELETE /api/plan-sessions/me/card` (`cards.edit`: it assigns the card to the session's agent): the session holds a card on no plan.
|
|
40
|
-
card_connect: needs("cards.edit"),
|
|
38
|
+
session_connect: BOOTSTRAP,
|
|
41
39
|
request_permission: BOOTSTRAP,
|
|
42
40
|
"50arms_api": BOOTSTRAP,
|
|
43
41
|
"50arms_api_list": BOOTSTRAP,
|
|
@@ -70,7 +68,7 @@ export const TOOL_ACCESS = {
|
|
|
70
68
|
// `src/tokens/tool-defs.ts`) granted to the `orchestrator` profile alone, not this table.
|
|
71
69
|
plan_events_wait: needs("plans.view"),
|
|
72
70
|
// DX-4632 - the plugin's session-start hook asks which plan the session this one replaced was working. A restarted session boots
|
|
73
|
-
// signed out, so it holds no permission: like `
|
|
71
|
+
// signed out, so it holds no permission: like `session_connect` it is a bootstrap tool, and a person's session's only (a dispatch
|
|
74
72
|
// never loads the plugin that calls it). The lookup asks the dashboard as the predecessor, with the credential the predecessor's
|
|
75
73
|
// record names.
|
|
76
74
|
restart_notice: OPERATOR_SESSION_BOOTSTRAP,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@50arms/platform-mcp",
|
|
3
|
-
"version": "0.1.
|
|
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 (
|
|
3
|
+
"version": "0.1.444",
|
|
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 (session_connect, request_permission, attach_file, and 50arms_slack_reply) (DX-3465, PLN-11 R-25).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|