@50arms/platform-mcp 0.0.0-stage → 0.1.436
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 +114 -2
- package/dist/access-request.js +101 -0
- package/dist/agent-gate.js +69 -0
- package/dist/board-id.js +27 -0
- package/dist/bounded-retry.js +25 -0
- package/dist/checkout-docs-root.js +54 -0
- package/dist/clear-control.js +77 -0
- package/dist/compaction-control.js +142 -0
- package/dist/credential.js +104 -0
- package/dist/danxbot-api-examples.js +97 -0
- package/dist/dashboard-identity.js +14 -0
- package/dist/design-screenshot.js +138 -0
- package/dist/digest-hold.js +67 -0
- package/dist/docs-tools.js +140 -0
- package/dist/durable-write.js +56 -0
- package/dist/entrypoint.js +35 -0
- package/dist/forbidden-hint.js +23 -0
- package/dist/guarded-calls.js +21 -0
- package/dist/handlers.js +385 -0
- package/dist/heartbeat-reporter.js +156 -0
- package/dist/http-client.js +190 -0
- package/dist/index.js +1128 -0
- package/dist/json-body.js +12 -0
- package/dist/key-revoked-halt.js +30 -0
- package/dist/lifecycle.js +113 -0
- package/dist/listen.js +999 -0
- package/dist/listener-reminder.js +119 -0
- package/dist/machine-daemon/autostart.js +160 -0
- package/dist/machine-daemon/beat.js +77 -0
- package/dist/machine-daemon/cli.js +42 -0
- package/dist/machine-daemon/install.js +168 -0
- package/dist/machine-daemon/state.js +64 -0
- package/dist/machine-daemon/version.js +13 -0
- package/dist/machine-daemon.js +553 -0
- package/dist/machine-toolkit.js +460 -0
- package/dist/mini-app-build.js +159 -0
- package/dist/mini-app-checkout.js +215 -0
- package/dist/mini-app-output.js +111 -0
- package/dist/mini-app-publish.js +154 -0
- package/dist/mini-app-restore.js +175 -0
- package/dist/one-line.js +25 -0
- package/dist/pacing-relay.js +16 -0
- package/dist/permission-request.js +134 -0
- package/dist/permission-watch.js +145 -0
- package/dist/plan-events-relay.js +572 -0
- package/dist/plan-events-tool.js +9 -0
- package/dist/plugin-publish.js +38 -0
- package/dist/plugin-screenshot.js +81 -0
- package/dist/request-bound.js +21 -0
- package/dist/restart-notice.js +271 -0
- package/dist/result-limit.js +31 -0
- package/dist/role-instruction-file.js +36 -0
- package/dist/route-board-scope.generated.js +93 -0
- package/dist/route-board-scope.js +71 -0
- package/dist/session-access.js +322 -0
- package/dist/session-connection.js +264 -0
- package/dist/session-control-file.js +58 -0
- package/dist/session-key.js +125 -0
- package/dist/session-options.js +60 -0
- package/dist/session-stats.js +235 -0
- package/dist/session-stream.js +345 -0
- package/dist/session-title.js +129 -0
- package/dist/shutdown.js +38 -0
- package/dist/stop-fixes.js +50 -0
- package/dist/subagents-live.js +162 -0
- package/dist/terms-halt.js +39 -0
- package/dist/text-limit.js +24 -0
- package/dist/tool-access.js +114 -0
- package/dist/tool-defs.js +58 -0
- package/dist/tool-surface.js +59 -0
- package/dist/transcript-lineage.js +105 -0
- package/dist/types.js +45 -0
- package/dist/urgent-records.js +47 -0
- package/dist/version-floor.js +146 -0
- package/package.json +51 -4
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-2920 — the ONE place that reads a JSON body's top-level `.error` string.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. the stream client (`session-stream.ts`, classifying a mint refusal such as `409 session_is_worker`) reads a refusal's
|
|
5
|
+
* error code from the parsed body, and this is the one place that decides what "carries an error code" means. Generic: it knows no
|
|
6
|
+
* dashboard error name, only that a JSON object may carry a top-level string field named `error`.
|
|
7
|
+
*/
|
|
8
|
+
export function errorCodeOf(body) {
|
|
9
|
+
return typeof body === "object" && body !== null && typeof body.error === "string"
|
|
10
|
+
? body.error
|
|
11
|
+
: null;
|
|
12
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The revocation a `401 key_revoked` result carries, or null for any other result. A `key_revoked` refusal that does not
|
|
3
|
+
* say who revoked it and when is a contract breach and throws, like a terms refusal without its link.
|
|
4
|
+
*/
|
|
5
|
+
export function keyRevocationOf(result) {
|
|
6
|
+
if (result.ok || result.status !== 401)
|
|
7
|
+
return null;
|
|
8
|
+
const body = result.body;
|
|
9
|
+
if (body?.error !== "key_revoked")
|
|
10
|
+
return null;
|
|
11
|
+
if (typeof body.revokedBy !== "string" || typeof body.revokedAt !== "string") {
|
|
12
|
+
throw new Error("[platform-mcp] a key_revoked refusal carried no revokedBy / revokedAt");
|
|
13
|
+
}
|
|
14
|
+
return { revokedBy: body.revokedBy, revokedAt: body.revokedAt };
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The one halt. `dispatched` is a dispatch's own credential: it makes no further call (every dashboard route, the
|
|
18
|
+
* dispatch-complete one included, refuses a revoked key; DX-4525), because the worker finds the revocation itself and ends the
|
|
19
|
+
* dispatch as revoked, not as the agent's failure (`src/worker/dispatch-execution.ts`). An operator session ends by telling the
|
|
20
|
+
* person; it has no tool that ends it.
|
|
21
|
+
*/
|
|
22
|
+
export function keyRevokedHalt(revocation, dispatched) {
|
|
23
|
+
const ending = dispatched
|
|
24
|
+
? "Then stop and make no further call: the worker ends this dispatch itself and records it as revoked, not as a failure."
|
|
25
|
+
: "Then end this session: tell the person what you committed and stop.";
|
|
26
|
+
return (`STOP ALL WORK NOW. ${revocation.revokedBy} revoked your access to the 50 Arms dashboard at ${revocation.revokedAt}.\n\n` +
|
|
27
|
+
"Commit your work in progress now (commit what you have; do not run agent-finalize.sh and do not merge). " +
|
|
28
|
+
`${ending}\n\n` +
|
|
29
|
+
"Do not call plan_connect or any other 50 Arms dashboard tool again, do not request access again and do not look for another route.");
|
|
30
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-3465 (PLN-11 R-25) / DX-4525 - the "lifecycle" tool set: the one agent-specific dispatch callback that is
|
|
3
|
+
* still a dedicated tool, `50arms_slack_reply`.
|
|
4
|
+
*
|
|
5
|
+
* Every other dispatch callback is a `50arms_api` call: an agent makes
|
|
6
|
+
* the dispatch-complete, gate sign-off, evaluator-summary, template-app load / save, Slack update and
|
|
7
|
+
* recovery calls through it (`src/dispatch/callback-routes.ts` names each route), so a dispatch learns ONE channel to the dashboard and reads its
|
|
8
|
+
* spec instead of carrying a tool schema per job. This one stays a tool because it reads local files (`files[]`) and
|
|
9
|
+
* uploads their bytes, which no `50arms_api` call can do.
|
|
10
|
+
*
|
|
11
|
+
* Registered UNCONDITIONALLY at module load (so the DX-1606 tool-defs generator, which never calls `boot()`, still
|
|
12
|
+
* introspects it for token-cost accounting). Whether a session SEES it is decided by `toolsFor` (`tool-access.ts`) from the
|
|
13
|
+
* key's permissions and the callback URL present (`presentCallbacksFromEnv`), applied by `tool-surface.ts` (DX-4523).
|
|
14
|
+
*/
|
|
15
|
+
import { readFile, stat } from "node:fs/promises";
|
|
16
|
+
import { basename, isAbsolute } from "node:path";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
import { CALLBACK_ENV } from "./tool-access.js";
|
|
19
|
+
import { CREDENTIAL_TOKEN_ENV } from "./credential.js";
|
|
20
|
+
/** The callbacks whose URL the worker injected into this process's environment (an empty value reads as absent): what `toolsFor` takes as this dispatch's callbacks. */
|
|
21
|
+
export function presentCallbacksFromEnv() {
|
|
22
|
+
return new Set(Object.keys(CALLBACK_ENV).filter((name) => (process.env[CALLBACK_ENV[name]] ?? "") !== ""));
|
|
23
|
+
}
|
|
24
|
+
async function postJson(url, body, bearer) {
|
|
25
|
+
return fetch(url, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${bearer}` },
|
|
28
|
+
body: JSON.stringify(body),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* DX-4525 - the dispatch's own credential, the one `50arms_api` calls the dashboard with: a dispatch declares it in
|
|
33
|
+
* `DANX_DASHBOARD_CREDENTIAL` (`credential.ts`) and it never signs out. Read here, not through the late-bound `SessionAccess`,
|
|
34
|
+
* so the tool works wherever the module is imported; a missing one is a loud failure, never an unauthenticated post.
|
|
35
|
+
*/
|
|
36
|
+
function dispatchCredential(calledAs) {
|
|
37
|
+
const token = process.env[CREDENTIAL_TOKEN_ENV];
|
|
38
|
+
if (typeof token !== "string" || token.trim() === "") {
|
|
39
|
+
throw new Error(`${calledAs}: ${CREDENTIAL_TOKEN_ENV} is not set for this dispatch`);
|
|
40
|
+
}
|
|
41
|
+
return token.trim();
|
|
42
|
+
}
|
|
43
|
+
/** Must match `MAX_DECODED_BYTES` in danxbot's `src/issues/attachment-limits.ts` (duplicated, not imported: this package is published on its own). */
|
|
44
|
+
const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
45
|
+
/**
|
|
46
|
+
* DX-4251 — read each attached path HERE, in the MCP server process beside the
|
|
47
|
+
* agent, and return its bytes. The dashboard that serves the reply route can
|
|
48
|
+
* run on a different host than the agent (dedicated topology), so a path could
|
|
49
|
+
* never be resolved over there.
|
|
50
|
+
*/
|
|
51
|
+
async function readAttachments(paths) {
|
|
52
|
+
if (paths === undefined)
|
|
53
|
+
return [];
|
|
54
|
+
return Promise.all(paths.map(async (entry, idx) => {
|
|
55
|
+
if (!isAbsolute(entry)) {
|
|
56
|
+
throw new Error(`field "files"[${idx}] must be an absolute path (got "${entry}")`);
|
|
57
|
+
}
|
|
58
|
+
const info = await stat(entry);
|
|
59
|
+
// DX-4251: a directory stats as size 0 (Windows) or 4096 (Linux); name it, never read it as a file.
|
|
60
|
+
if (!info.isFile())
|
|
61
|
+
throw new Error(`field "files"[${idx}] "${entry}" is not a file`);
|
|
62
|
+
if (info.size === 0)
|
|
63
|
+
throw new Error(`field "files"[${idx}] "${entry}" is an empty file`);
|
|
64
|
+
if (info.size > MAX_ATTACHMENT_BYTES) {
|
|
65
|
+
throw new Error(`field "files"[${idx}] "${entry}" is ${info.size} bytes, over the ${MAX_ATTACHMENT_BYTES}-byte attachment limit`);
|
|
66
|
+
}
|
|
67
|
+
return { filename: basename(entry), base64: (await readFile(entry)).toString("base64") };
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
/** A refused callback's body is the only place the server says why — relay it, never just the status. */
|
|
71
|
+
async function responseFailure(label, response) {
|
|
72
|
+
const text = await response.text();
|
|
73
|
+
return new Error(`${label} returned HTTP ${response.status}${text ? `: ${text}` : ""}`);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Registers the lifecycle tool on `server` via the caller's `strictTool` wrapper (so it gets the same DX-3238 strict-shape
|
|
77
|
+
* guard the rest of this package's tools use).
|
|
78
|
+
*/
|
|
79
|
+
export function registerLifecycleTools(renamedTool) {
|
|
80
|
+
// ---------------- 50arms_slack_reply (and `danxbot_slack_reply` until DX-6864) ----------------
|
|
81
|
+
renamedTool("50arms_slack_reply",
|
|
82
|
+
// DX-4253 — no ordering against another tool here: the Slack persona (`src/agent/persona.ts`) and the
|
|
83
|
+
// `slack-agent` profile own the order (reply, link, attach, complete).
|
|
84
|
+
"Post the final user-facing reply to the originating Slack thread, once per dispatch. Files in `files[]` " +
|
|
85
|
+
"post with the text as one thread message. For a progress update, `50arms_api` `POST /api/slack/update` `{text}`.", {
|
|
86
|
+
text: z.string().describe("The final reply text to post to the Slack thread."),
|
|
87
|
+
files: z
|
|
88
|
+
.array(z.string())
|
|
89
|
+
.optional()
|
|
90
|
+
.describe("Absolute paths of your own files to attach; bytes are uploaded."),
|
|
91
|
+
}, (calledAs) => async (args) => {
|
|
92
|
+
const url = process.env[CALLBACK_ENV.slackReply];
|
|
93
|
+
if (!url)
|
|
94
|
+
throw new Error(`${calledAs} called outside a Slack dispatch`);
|
|
95
|
+
const files = await readAttachments(args.files);
|
|
96
|
+
const body = { text: args.text };
|
|
97
|
+
if (files.length > 0)
|
|
98
|
+
body.files = files;
|
|
99
|
+
const response = await postJson(url, body, dispatchCredential(calledAs));
|
|
100
|
+
if (!response.ok)
|
|
101
|
+
throw await responseFailure("Slack reply API", response);
|
|
102
|
+
return {
|
|
103
|
+
content: [
|
|
104
|
+
{
|
|
105
|
+
type: "text",
|
|
106
|
+
text: files.length > 0
|
|
107
|
+
? `Reply posted to Slack thread with ${files.length} file(s)`
|
|
108
|
+
: "Reply posted to Slack thread",
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|