@opvs-ai/mcp 2.6.1 → 2.7.1
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 +71 -3
- package/dist/.build-stamp.json +9 -0
- package/dist/board.d.ts +66 -0
- package/dist/board.js +175 -0
- package/dist/board.js.map +1 -0
- package/dist/factory-guidance.d.ts +14 -0
- package/dist/factory-guidance.js +27 -0
- package/dist/factory-guidance.js.map +1 -0
- package/dist/fleet.d.ts +169 -0
- package/dist/fleet.js +474 -0
- package/dist/fleet.js.map +1 -0
- package/dist/http.d.ts +39 -0
- package/dist/http.js +83 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +16 -2
- package/dist/index.js +34 -136
- package/dist/index.js.map +1 -1
- package/package.json +10 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @opvs-ai/mcp
|
|
2
2
|
|
|
3
|
-
OPVS MCP server for the **on-demand model** —
|
|
3
|
+
OPVS MCP server for the **on-demand model** — 16 tools that give any MCP host (Claude Desktop, Cursor, Antigravity, Windsurf, Claude Code) the same surface OPVS-hosted agents see: 10 always-on marketplace tools, `sandbox_chat` (a vendor test mode), and 5 **fleet** tools for machines and slots. Everything else is reached through `tool_call` indirection so the package fits under the ~100-tool cap every current MCP client enforces.
|
|
4
4
|
|
|
5
5
|
| Tool | Wraps | Purpose |
|
|
6
6
|
|------|-------|---------|
|
|
@@ -13,7 +13,53 @@ OPVS MCP server for the **on-demand model** — 10 always-on tools that give any
|
|
|
13
13
|
| `tool_install` | `/marketplace/packages/{id}/install` | Add a package with explicit consent |
|
|
14
14
|
| `tool_uninstall` | `DELETE /marketplace/installed/{id}` | Remove a package |
|
|
15
15
|
| `whoami` | `/auth/me` | Session probe — brand, agent, workspace |
|
|
16
|
-
| `board_as_markdown` | `/board/boards/{id}/export?format=md` | A whole AgentBoard as a Markdown plan in one call (`detail` summary/standard/full, `columns` filter) |
|
|
16
|
+
| `board_as_markdown` | `/board/boards/{id}/export?format=md` | A whole AgentBoard as a Markdown plan in one call (`detail` summary/standard/full, `columns` filter). A board over the server's 1 MiB export ceiling is **refused, not truncated** — the refusal carries `actual_bytes`/`limit_bytes`/`retry_with` as fields. It does **not** silently retry at a lower detail: the tool promises these exact bytes, so substituting a different document is the caller's decision |
|
|
17
|
+
|
|
18
|
+
### Fleet — machines and slots
|
|
19
|
+
|
|
20
|
+
The environment layer had **no agent-reachable surface at all** until these landed, so
|
|
21
|
+
"dashboard or MCP" was dashboard-only. They are native tools rather than `tool_call` routes
|
|
22
|
+
for the same reason `board_as_markdown` is: there is no marketplace skill for environments,
|
|
23
|
+
so there is no `schema.yaml` to route through.
|
|
24
|
+
|
|
25
|
+
| Tool | Wraps | Purpose |
|
|
26
|
+
|------|-------|---------|
|
|
27
|
+
| `fleet_list` | `/environments` + `/environments/allowance` | Every slot this brand owns + the machine allowance it spends, grouped by machine. `detail` compact/full |
|
|
28
|
+
| `slot_get` | `/environments/{ref}` → `/workstations/{uuid}` | One slot + its per-step onboarding checklist, and `next_action` — the single step to act on |
|
|
29
|
+
| `slot_health` | `/environments/{ref}/health` | **Live** sidecar probe (reaches the box over WireGuard) |
|
|
30
|
+
| `fleet_logins` | `/environments/credentials/reauth-status` | Lapsed logins on both credential lanes, reported separately |
|
|
31
|
+
| `slot_order` | `POST /workstations` | Order a slot — **writes**: spends the allowance, mints a one-shot install PAT |
|
|
32
|
+
|
|
33
|
+
#### Vocabulary
|
|
34
|
+
|
|
35
|
+
At the product surface there are exactly two nouns, and the tool names use them
|
|
36
|
+
([`MACHINE-AND-SLOT.md`](../../docs/planning/opvsRunner/MACHINE-AND-SLOT.md) §3, owner ruling
|
|
37
|
+
2026-09-07):
|
|
38
|
+
|
|
39
|
+
- **MACHINE** — the physical host. What a tunnel terminates at. Keyed by `machine_id`.
|
|
40
|
+
- **SLOT** — the worker at a desk on it. What a card is claimed by, what a terminal opens into.
|
|
41
|
+
|
|
42
|
+
**On the wire it is still `environments`**, and that disagreement is deliberate — renaming a
|
|
43
|
+
live table, its router and its TS type buys nothing a reader needs. The words *box* and *desk*
|
|
44
|
+
are retired from new writing but appear throughout the older docs. Every tool description names
|
|
45
|
+
the synonyms, so an agent told "list my environments" or "check my box" still finds the tool.
|
|
46
|
+
|
|
47
|
+
#### Three things that surprise people
|
|
48
|
+
|
|
49
|
+
1. **Any ref shape works on `slot_get` and `slot_health`.** Full UUID, an 8-or-more hex prefix,
|
|
50
|
+
or the `env_…` short code. `slot_get` resolves the ref *first* and then uses the canonical
|
|
51
|
+
UUID for the checklist hop — because `/workstations/{id}` accepts **only** a UUID and 404s a
|
|
52
|
+
short code, which would otherwise read as "your slot does not exist".
|
|
53
|
+
2. **A `null` is never a green light.** `allowance: null` is paired with `allowance_error`;
|
|
54
|
+
`onboarding: null` with `onboarding_error`. Both mean *unknown*, never *fine* — do not tell
|
|
55
|
+
an operator they have room, or that nothing is outstanding, on the strength of a null.
|
|
56
|
+
3. **`fleet_logins` reports two lanes and never sums them.** `lease` is the build credential
|
|
57
|
+
(lapsed → builds fall back to metered pricing); `desk` is what the box's terminal opens
|
|
58
|
+
(lapsed → the desk will not start). A box can be perfect on one and unusable on the other,
|
|
59
|
+
so a single total could not say which broke. Desk state is four-valued —
|
|
60
|
+
`healthy | not_applicable | unknown | lapsed` — and "nobody asked OPVS to manage this desk"
|
|
61
|
+
is a different sentence from "OPVS was asked and cannot vouch for it".
|
|
62
|
+
|
|
17
63
|
|
|
18
64
|
All tool names match `^[a-zA-Z0-9_-]+$` (the OpenAI tool-name regex; dots break Lola/Roni/planner on first call).
|
|
19
65
|
|
|
@@ -149,7 +195,7 @@ Every tool returns a single text-content block whose payload is JSON with:
|
|
|
149
195
|
- **Optional flat sibling `_findings: [...]`** — only when the server emits one (e.g. `audit_level=all`).
|
|
150
196
|
- **`telemetry_signal`** in search results — closed enum: `absent | cold | warm | hot`.
|
|
151
197
|
|
|
152
|
-
The contract was locked with SpiderIQ on 2026-05-15 PM. See [`docs/planning/Marketplace/
|
|
198
|
+
The contract was locked with SpiderIQ on 2026-05-15 PM. See [`docs/planning/Marketplace/CLAUDE.md`](../../docs/planning/Marketplace/CLAUDE.md#architecture) for the gateway-side semantics this MCP mirrors.
|
|
153
199
|
|
|
154
200
|
## tool_pin status
|
|
155
201
|
|
|
@@ -175,6 +221,28 @@ The on-demand model (10 always-on + `tool_call` indirection) reaches the same sk
|
|
|
175
221
|
|
|
176
222
|
Versions ≤ 0.7.4 are end-of-life. There is no `@opvs-ai/mcp@0.x → 1.0` migration code path — replace the old install with a fresh one.
|
|
177
223
|
|
|
224
|
+
## Development
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
cd ../core && npm install && npx tsc # REQUIRED FIRST — see below
|
|
228
|
+
cd ../mcp && npm install
|
|
229
|
+
npx vitest run # unit tests
|
|
230
|
+
npm run build && OPVS_TOKEN=smoke-test npm run list-tools
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Two things bite from a clean checkout:
|
|
234
|
+
|
|
235
|
+
- **Build `@opvs-ai/core` first.** Its `main` points at `dist/`, so without that step the
|
|
236
|
+
suite dies at *collection* with `Failed to resolve entry for package "@opvs-ai/core"` —
|
|
237
|
+
a missing build, not a broken suite.
|
|
238
|
+
- **`npm run list-tools` is a real gate, not a printout.** It drives the actual stdio
|
|
239
|
+
protocol against the built server and exits non-zero on a tool name that fails
|
|
240
|
+
`^[a-zA-Z0-9_-]+$` (exit 2) or on going over the tool-count ceiling (exit 3). A unit test
|
|
241
|
+
of the `TOOLS` array cannot prove a tool is *registered*; this can. Both run in CI under
|
|
242
|
+
**TS Library Tests**.
|
|
243
|
+
|
|
244
|
+
Counts move. Measure them at your SHA rather than citing a number from here.
|
|
245
|
+
|
|
178
246
|
## Related packages
|
|
179
247
|
|
|
180
248
|
- [`@opvs-ai/cli`](../cli) — terminal access (no MCP layer)
|
package/dist/board.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Board tools — `board_as_markdown` (Topic 1 / S1.3).
|
|
3
|
+
*
|
|
4
|
+
* Lives outside `index.ts` because that file ends in a top-level
|
|
5
|
+
* `await server.connect(transport)` — importing it to unit-test a handler would
|
|
6
|
+
* start a server. See `http.ts` and `fleet.ts` for the same reasoning (OSB-119).
|
|
7
|
+
*
|
|
8
|
+
* Carved out at OSB-207, whose whole subject is what this tool does when the
|
|
9
|
+
* export is REFUSED, and which could not be tested where it was.
|
|
10
|
+
*/
|
|
11
|
+
import { type ToolResult } from "./http.js";
|
|
12
|
+
export declare const BOARD_TOOLS: {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: string;
|
|
17
|
+
properties: {
|
|
18
|
+
board_id: {
|
|
19
|
+
type: string;
|
|
20
|
+
description: string;
|
|
21
|
+
};
|
|
22
|
+
detail: {
|
|
23
|
+
type: string;
|
|
24
|
+
enum: string[];
|
|
25
|
+
description: string;
|
|
26
|
+
};
|
|
27
|
+
columns: {
|
|
28
|
+
type: string;
|
|
29
|
+
description: string;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
required: string[];
|
|
33
|
+
};
|
|
34
|
+
}[];
|
|
35
|
+
/** The shape `@opvs-ai/core` throws, plus the parsed body it now carries. */
|
|
36
|
+
type UpstreamError = {
|
|
37
|
+
detail?: string;
|
|
38
|
+
status?: number;
|
|
39
|
+
body?: unknown;
|
|
40
|
+
};
|
|
41
|
+
/** The structured half of AgentBoard's 413, when it sent one. */
|
|
42
|
+
type ExportRefusal = {
|
|
43
|
+
error?: unknown;
|
|
44
|
+
message?: unknown;
|
|
45
|
+
actual_bytes?: unknown;
|
|
46
|
+
limit_bytes?: unknown;
|
|
47
|
+
detail_level?: unknown;
|
|
48
|
+
retry_with?: unknown;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Pull AgentBoard's structured refusal out of a thrown error, if it sent one.
|
|
52
|
+
*
|
|
53
|
+
* Returns `null` for the PROSE shape — which is not a failure to handle but the
|
|
54
|
+
* ordinary state during a deploy window: this package and AgentBoard ship on
|
|
55
|
+
* different schedules, so a rebuilt MCP will spend some hours talking to a
|
|
56
|
+
* server that still answers `{"detail": "<sentence>"}`. The caller must stay
|
|
57
|
+
* useful on that path, so nothing here parses the sentence for numbers — an
|
|
58
|
+
* absent field is honest, a number scraped out of English is a guess.
|
|
59
|
+
*/
|
|
60
|
+
export declare function exportRefusalFields(e: UpstreamError): ExportRefusal | null;
|
|
61
|
+
export declare function handleBoardAsMarkdown(args: {
|
|
62
|
+
board_id: string;
|
|
63
|
+
detail?: string;
|
|
64
|
+
columns?: string;
|
|
65
|
+
}): Promise<ToolResult>;
|
|
66
|
+
export {};
|
package/dist/board.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Board tools — `board_as_markdown` (Topic 1 / S1.3).
|
|
3
|
+
*
|
|
4
|
+
* Lives outside `index.ts` because that file ends in a top-level
|
|
5
|
+
* `await server.connect(transport)` — importing it to unit-test a handler would
|
|
6
|
+
* start a server. See `http.ts` and `fleet.ts` for the same reasoning (OSB-119).
|
|
7
|
+
*
|
|
8
|
+
* Carved out at OSB-207, whose whole subject is what this tool does when the
|
|
9
|
+
* export is REFUSED, and which could not be tested where it was.
|
|
10
|
+
*/
|
|
11
|
+
import { err, errFields, getText, ok } from "./http.js";
|
|
12
|
+
export const BOARD_TOOLS = [
|
|
13
|
+
{
|
|
14
|
+
name: "board_as_markdown",
|
|
15
|
+
description: "Read a WHOLE AgentBoard (or a chosen subset of its columns) as a single " +
|
|
16
|
+
"Markdown plan/PRD document — one call instead of N task fetches. Cheaper on " +
|
|
17
|
+
"tokens and gives narrative context, not raw rows. Returns the rendered " +
|
|
18
|
+
"Markdown string verbatim (read-only, deterministic — same board state → same " +
|
|
19
|
+
"bytes, each task row carries an HTML comment with its UUID for diffing). " +
|
|
20
|
+
"Brand is pinned server-side from the PAT — never pass a brand_id. Pick `detail`: " +
|
|
21
|
+
"`summary` = compact one-row-per-task table (token-cheapest); `standard` " +
|
|
22
|
+
"(default) = the nested plan format with task bodies, subtasks and stages; " +
|
|
23
|
+
"`full` = the exhaustive dump including comments. " +
|
|
24
|
+
"SIZE: a big board can exceed the server's 1 MiB export ceiling at `standard` " +
|
|
25
|
+
"or `full` and is then REFUSED (413) rather than truncated — the refusal is " +
|
|
26
|
+
"machine-readable (`actual_bytes`, `limit_bytes`, `retry_with`) and this tool " +
|
|
27
|
+
"does NOT silently retry at a lower detail, because it promises these exact " +
|
|
28
|
+
"bytes. On a board you know is large, ask for `detail='summary'` up front: it " +
|
|
29
|
+
"is the one narrowing that drops no card.",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
board_id: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "The board UUID to render.",
|
|
36
|
+
},
|
|
37
|
+
detail: {
|
|
38
|
+
type: "string",
|
|
39
|
+
enum: ["summary", "standard", "full"],
|
|
40
|
+
description: "Render depth. Default `standard` (the nested plan/PRD shape). " +
|
|
41
|
+
"`summary` is the compact table; `full` adds comments.",
|
|
42
|
+
},
|
|
43
|
+
columns: {
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "Optional comma-separated column filter — names OR ids, mixed. Each token " +
|
|
46
|
+
"is resolved name-first-then-id; an unknown token fails loud (400). Omit to " +
|
|
47
|
+
"render every column.",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
required: ["board_id"],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
/**
|
|
55
|
+
* Pull AgentBoard's structured refusal out of a thrown error, if it sent one.
|
|
56
|
+
*
|
|
57
|
+
* Returns `null` for the PROSE shape — which is not a failure to handle but the
|
|
58
|
+
* ordinary state during a deploy window: this package and AgentBoard ship on
|
|
59
|
+
* different schedules, so a rebuilt MCP will spend some hours talking to a
|
|
60
|
+
* server that still answers `{"detail": "<sentence>"}`. The caller must stay
|
|
61
|
+
* useful on that path, so nothing here parses the sentence for numbers — an
|
|
62
|
+
* absent field is honest, a number scraped out of English is a guess.
|
|
63
|
+
*/
|
|
64
|
+
export function exportRefusalFields(e) {
|
|
65
|
+
const body = e.body;
|
|
66
|
+
const detail = body?.detail;
|
|
67
|
+
if (!detail || typeof detail !== "object" || Array.isArray(detail))
|
|
68
|
+
return null;
|
|
69
|
+
const d = detail;
|
|
70
|
+
return d.error === "markdown_export_too_large" ? d : null;
|
|
71
|
+
}
|
|
72
|
+
function asNumber(v) {
|
|
73
|
+
return typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Turn a 413 into a refusal an agent can act on without reading English.
|
|
77
|
+
*
|
|
78
|
+
* DECISION (OSB-207): this does NOT retry at `summary` itself.
|
|
79
|
+
*
|
|
80
|
+
* The tool's contract is "the rendered Markdown string verbatim — same board
|
|
81
|
+
* state → same bytes", which is what makes the output safe to commit and diff.
|
|
82
|
+
* There is no envelope on the success path to announce a substitution in, so
|
|
83
|
+
* auto-retrying would have to either corrupt those bytes with a banner or hand
|
|
84
|
+
* back a compact table where task bodies were asked for — and an agent that
|
|
85
|
+
* receives a table silently has no way to tell "the cards have no descriptions"
|
|
86
|
+
* from "I was served a different document than I requested". The cost of not
|
|
87
|
+
* retrying is one round-trip; the cost of retrying is a wrong conclusion that
|
|
88
|
+
* looks like data. The numbers and the exact next call go in the refusal
|
|
89
|
+
* instead, so that round-trip is always correct.
|
|
90
|
+
*/
|
|
91
|
+
function tooLargeResult(boardId, requested, e) {
|
|
92
|
+
const fields = exportRefusalFields(e);
|
|
93
|
+
const actual = asNumber(fields?.actual_bytes);
|
|
94
|
+
const limit = asNumber(fields?.limit_bytes);
|
|
95
|
+
// The server reports the level it refused; fall back to the one WE asked
|
|
96
|
+
// for, which is known here and is not a guess. Undefined only when the
|
|
97
|
+
// caller omitted `detail` AND the server sent prose — we do not assert the
|
|
98
|
+
// server's default on its behalf.
|
|
99
|
+
const level = typeof fields?.detail_level === "string" ? fields.detail_level : requested;
|
|
100
|
+
const sizes = actual != null && limit != null
|
|
101
|
+
? ` — ${actual.toLocaleString("en-US")} bytes, over the ${limit.toLocaleString("en-US")}-byte limit`
|
|
102
|
+
: "";
|
|
103
|
+
const at = level ? ` at detail='${level}'` : "";
|
|
104
|
+
// `summary` is exempt from the ceiling server-side, so this branch is
|
|
105
|
+
// unreachable today. It exists because the alternative is guidance that
|
|
106
|
+
// names the level that just failed — the "a ceiling that can reject its own
|
|
107
|
+
// remedy is a dead end" shape, one layer out. Cheap to be right about.
|
|
108
|
+
const summaryAlsoRefused = level === "summary";
|
|
109
|
+
const guidance = summaryAlsoRefused
|
|
110
|
+
? "#audit.error:too-large — even detail='summary' exceeds the server's " +
|
|
111
|
+
"Markdown export ceiling for this board, so there is no lossless export " +
|
|
112
|
+
"of the whole board. This is NOT an auth, brand or board_id problem. " +
|
|
113
|
+
`#next:board_as_markdown(board_id='${boardId}', detail='summary', ` +
|
|
114
|
+
"columns='<name,name>') — a SUBSET, lossy: cards outside the named " +
|
|
115
|
+
"columns are absent from the document entirely. Use " +
|
|
116
|
+
"tool_call(@opvs-ai/agentboard, list_columns) for the valid names."
|
|
117
|
+
: "#audit.error:too-large — the board exceeds the server's Markdown export " +
|
|
118
|
+
"ceiling at this detail level. This is NOT an auth, brand or board_id " +
|
|
119
|
+
"problem, and retrying the identical call will fail identically. " +
|
|
120
|
+
`#next:board_as_markdown(board_id='${boardId}', detail='summary') — a ` +
|
|
121
|
+
"compact one-row-per-task table over EVERY card, each keeping its " +
|
|
122
|
+
"<!-- task: uuid --> anchor, so no card is dropped. Pass " +
|
|
123
|
+
"columns='<name,name>' instead to export a SUBSET — lossy: cards outside " +
|
|
124
|
+
"the named columns are absent from the document entirely.";
|
|
125
|
+
return errFields(`board_as_markdown: board '${boardId}' is too large to export${at}${sizes}.`, {
|
|
126
|
+
code: "markdown_export_too_large",
|
|
127
|
+
...(actual != null ? { actual_bytes: actual } : {}),
|
|
128
|
+
...(limit != null ? { limit_bytes: limit } : {}),
|
|
129
|
+
...(level != null ? { detail_level: level } : {}),
|
|
130
|
+
// The one narrowing that drops no card, as a parameter not a sentence —
|
|
131
|
+
// omitted when it is the thing that was just refused.
|
|
132
|
+
...(summaryAlsoRefused ? {} : { retry_with: { detail: "summary" } }),
|
|
133
|
+
lossy_alternative: { columns: "<name,name>" },
|
|
134
|
+
}, guidance);
|
|
135
|
+
}
|
|
136
|
+
export async function handleBoardAsMarkdown(args) {
|
|
137
|
+
if (!args.board_id || typeof args.board_id !== "string") {
|
|
138
|
+
return err("board_id is required", "#audit.error:input=missing — board_as_markdown needs the board UUID. " +
|
|
139
|
+
"Use tool_call(@opvs-ai/agentboard, list_boards) to find one.");
|
|
140
|
+
}
|
|
141
|
+
if (args.detail != null &&
|
|
142
|
+
!["summary", "standard", "full"].includes(args.detail)) {
|
|
143
|
+
return err(`invalid detail '${args.detail}'`, "#audit.error:input=bad-detail — detail must be summary | standard | full. " +
|
|
144
|
+
"#next:omit it for the default (standard).");
|
|
145
|
+
}
|
|
146
|
+
// Mirror the export contract: format=md is fixed; detail/columns are forwarded
|
|
147
|
+
// only when the caller set them (don't synthesize defaults — let the backend
|
|
148
|
+
// own them; LEARNINGS #38, the silent-drop tax cuts both ways).
|
|
149
|
+
const q = new URLSearchParams({ format: "md" });
|
|
150
|
+
if (args.detail)
|
|
151
|
+
q.set("detail", args.detail);
|
|
152
|
+
if (args.columns)
|
|
153
|
+
q.set("columns", args.columns);
|
|
154
|
+
const path = `/api/v1/board/boards/${encodeURIComponent(args.board_id)}/export?${q.toString()}`;
|
|
155
|
+
try {
|
|
156
|
+
const md = await getText(path);
|
|
157
|
+
return ok(md);
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
const error = e;
|
|
161
|
+
if (error.status === 413) {
|
|
162
|
+
return tooLargeResult(args.board_id, args.detail, error);
|
|
163
|
+
}
|
|
164
|
+
if (error.status === 400) {
|
|
165
|
+
return err(`board_as_markdown: ${error.detail ?? "bad request"}`, "#audit.error:bad-columns — an unknown column name/id was passed. " +
|
|
166
|
+
"#next:tool_call(@opvs-ai/agentboard, list_columns) to see valid columns, or omit `columns`.");
|
|
167
|
+
}
|
|
168
|
+
if (error.status === 404) {
|
|
169
|
+
return err(`board '${args.board_id}' not found (or not visible to this brand)`, "#audit.error:board=missing #next:whoami to confirm the PAT's brand; " +
|
|
170
|
+
"tool_call(@opvs-ai/agentboard, list_boards) to list boards.");
|
|
171
|
+
}
|
|
172
|
+
return err(`board_as_markdown failed: ${error.status ?? "?"} ${error.detail ?? ""}`, "#audit.error:upstream #next:whoami to verify auth/brand; check the board_id.");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=board.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"board.js","sourceRoot":"","sources":["../src/board.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Routing nudge for board/factory skills (GS.2). A POINTER to the canonical
|
|
2
|
+
* sources (the skill's SKILL.md + its references, read with `tool_read`, and
|
|
3
|
+
* the live claimTask/getRunnable `_guidance` envelopes), carrying the few
|
|
4
|
+
* platform invariants an MCP agent needs at decision time so it can't drive a
|
|
5
|
+
* factory board blind. Deliberately terse — the full recipe is
|
|
6
|
+
* references/run-a-factory-board.md; this never restates it.
|
|
7
|
+
*
|
|
8
|
+
* ASP-73: this used to send agents to `section='description'` for "the
|
|
9
|
+
* operating model + references index". tool_help's description section is the
|
|
10
|
+
* registry's one-line package description, so the pointer led back to this
|
|
11
|
+
* same `_guidance` — a loop. The operating model is what `tool_read` serves.
|
|
12
|
+
* ASP-75: the one-active-project claim gate is stated here too, because it
|
|
13
|
+
* refuses every claim before any of the other invariants matter. */
|
|
14
|
+
export declare const FACTORY_HELP_GUIDANCE: string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Routing nudge for board/factory skills (GS.2). A POINTER to the canonical
|
|
2
|
+
* sources (the skill's SKILL.md + its references, read with `tool_read`, and
|
|
3
|
+
* the live claimTask/getRunnable `_guidance` envelopes), carrying the few
|
|
4
|
+
* platform invariants an MCP agent needs at decision time so it can't drive a
|
|
5
|
+
* factory board blind. Deliberately terse — the full recipe is
|
|
6
|
+
* references/run-a-factory-board.md; this never restates it.
|
|
7
|
+
*
|
|
8
|
+
* ASP-73: this used to send agents to `section='description'` for "the
|
|
9
|
+
* operating model + references index". tool_help's description section is the
|
|
10
|
+
* registry's one-line package description, so the pointer led back to this
|
|
11
|
+
* same `_guidance` — a loop. The operating model is what `tool_read` serves.
|
|
12
|
+
* ASP-75: the one-active-project claim gate is stated here too, because it
|
|
13
|
+
* refuses every claim before any of the other invariants matter. */
|
|
14
|
+
export const FACTORY_HELP_GUIDANCE = "#use:board/DAG/factory task pipelines. " +
|
|
15
|
+
"#read:tool_read(name) for the operating model (the skill's SKILL.md) — its " +
|
|
16
|
+
"`layers[]` lists every recipe; tool_read(name, path='references/run-a-factory-board.md') " +
|
|
17
|
+
"is the factory recipe. " +
|
|
18
|
+
"#factory:the board must be in exactly ONE active project (moveBoardToProject " +
|
|
19
|
+
"mode=move) or every claimTask returns task_id:null reason board_project_unresolved. " +
|
|
20
|
+
"New cards land in Backlog (staging, NOT claimable) → move to Ready " +
|
|
21
|
+
"(claimable by role 'builder') → a worker claims and it auto-moves to Executing. " +
|
|
22
|
+
"A factory board runs NOTHING until an opvs-sidecar worker is pointed at its " +
|
|
23
|
+
"board_id; set a `mode:` label (auto|assisted|manual) per card for build autonomy. " +
|
|
24
|
+
"#next:the live claimTask + getRunnable responses carry their own `_guidance` " +
|
|
25
|
+
"envelope (e.g. a claim that returns task_id:null+reason is a misconfig, " +
|
|
26
|
+
"not no-work) — read it.";
|
|
27
|
+
//# sourceMappingURL=factory-guidance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory-guidance.js","sourceRoot":"","sources":["../src/factory-guidance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;qEAYqE;AACrE,MAAM,CAAC,MAAM,qBAAqB,GAChC,yCAAyC;IACzC,6EAA6E;IAC7E,2FAA2F;IAC3F,yBAAyB;IACzB,+EAA+E;IAC/E,sFAAsF;IACtF,qEAAqE;IACrE,kFAAkF;IAClF,8EAA8E;IAC9E,oFAAoF;IACpF,+EAA+E;IAC/E,0EAA0E;IAC1E,yBAAyB,CAAC"}
|
package/dist/fleet.d.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fleet tools — the MACHINE and SLOT surface (OSB-119, BYO plan row 3.2).
|
|
3
|
+
*
|
|
4
|
+
* Until this module the MCP package had 11 tools and NONE of them reached the
|
|
5
|
+
* environment layer, so the owner's "dashboard or MCP" was dashboard-only.
|
|
6
|
+
*
|
|
7
|
+
* Lives outside `index.ts` because that file ends in a top-level
|
|
8
|
+
* `await server.connect(transport)` — importing it to unit-test a handler would
|
|
9
|
+
* start a server. See `http.ts` for the same reasoning.
|
|
10
|
+
*/
|
|
11
|
+
import { type ToolResult } from "./http.js";
|
|
12
|
+
export declare const FLEET_TOOLS: ({
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: string;
|
|
17
|
+
properties: {
|
|
18
|
+
detail: {
|
|
19
|
+
type: string;
|
|
20
|
+
enum: string[];
|
|
21
|
+
description: string;
|
|
22
|
+
};
|
|
23
|
+
slot?: undefined;
|
|
24
|
+
name?: undefined;
|
|
25
|
+
runtime?: undefined;
|
|
26
|
+
region?: undefined;
|
|
27
|
+
description?: undefined;
|
|
28
|
+
};
|
|
29
|
+
required?: undefined;
|
|
30
|
+
};
|
|
31
|
+
} | {
|
|
32
|
+
name: string;
|
|
33
|
+
description: string;
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: string;
|
|
36
|
+
properties: {
|
|
37
|
+
slot: {
|
|
38
|
+
type: string;
|
|
39
|
+
description: string;
|
|
40
|
+
};
|
|
41
|
+
detail?: undefined;
|
|
42
|
+
name?: undefined;
|
|
43
|
+
runtime?: undefined;
|
|
44
|
+
region?: undefined;
|
|
45
|
+
description?: undefined;
|
|
46
|
+
};
|
|
47
|
+
required: string[];
|
|
48
|
+
};
|
|
49
|
+
} | {
|
|
50
|
+
name: string;
|
|
51
|
+
description: string;
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: string;
|
|
54
|
+
properties: {
|
|
55
|
+
detail?: undefined;
|
|
56
|
+
slot?: undefined;
|
|
57
|
+
name?: undefined;
|
|
58
|
+
runtime?: undefined;
|
|
59
|
+
region?: undefined;
|
|
60
|
+
description?: undefined;
|
|
61
|
+
};
|
|
62
|
+
required?: undefined;
|
|
63
|
+
};
|
|
64
|
+
} | {
|
|
65
|
+
name: string;
|
|
66
|
+
description: string;
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: string;
|
|
69
|
+
properties: {
|
|
70
|
+
name: {
|
|
71
|
+
type: string;
|
|
72
|
+
description: string;
|
|
73
|
+
};
|
|
74
|
+
runtime: {
|
|
75
|
+
type: string;
|
|
76
|
+
enum: string[];
|
|
77
|
+
description: string;
|
|
78
|
+
};
|
|
79
|
+
region: {
|
|
80
|
+
type: string;
|
|
81
|
+
description: string;
|
|
82
|
+
};
|
|
83
|
+
description: {
|
|
84
|
+
type: string;
|
|
85
|
+
description: string;
|
|
86
|
+
};
|
|
87
|
+
detail?: undefined;
|
|
88
|
+
slot?: undefined;
|
|
89
|
+
};
|
|
90
|
+
required: string[];
|
|
91
|
+
};
|
|
92
|
+
})[];
|
|
93
|
+
/** The slot fields a compact fleet row carries.
|
|
94
|
+
*
|
|
95
|
+
* AN ENUMERATION IS ITSELF A CLAIM, so this list is exported into the
|
|
96
|
+
* `fleet_list` description verbatim and `detail: "full"` exists to escape it.
|
|
97
|
+
* A trim that nobody can see is indistinguishable from a field the server
|
|
98
|
+
* stopped sending.
|
|
99
|
+
*/
|
|
100
|
+
export declare const COMPACT_SLOT_FIELDS: readonly ["id", "short_id", "name", "status", "dormant", "runtime", "machine_id", "sidecar_port", "wg_address", "sidecar_version", "last_health_check", "last_sync_at", "pending_sync_count"];
|
|
101
|
+
export declare function compactSlot(row: Record<string, unknown>): Record<string, unknown>;
|
|
102
|
+
/** Group slots under the machine that reported hosting them.
|
|
103
|
+
*
|
|
104
|
+
* 🔴 A NULL `machine_id` is NOT a group. The column is written by the health
|
|
105
|
+
* poller from `/etc/machine-id` and is NULL whenever the sidecar is too old to
|
|
106
|
+
* report it — which is still the common case. Two boxes that both decline to
|
|
107
|
+
* say where they run are not thereby co-tenants, and collapsing them would
|
|
108
|
+
* invent a co-tenancy the platform has no evidence for
|
|
109
|
+
* (`schemas/environment.py`, `machine_id`). They go to `unplaced`, one entry
|
|
110
|
+
* each, in list order.
|
|
111
|
+
*/
|
|
112
|
+
export declare function groupByMachine(rows: Array<Record<string, unknown>>): {
|
|
113
|
+
machines: Array<{
|
|
114
|
+
machine_id: string;
|
|
115
|
+
slot_count: number;
|
|
116
|
+
slots: string[];
|
|
117
|
+
}>;
|
|
118
|
+
unplaced: string[];
|
|
119
|
+
};
|
|
120
|
+
export declare function handleFleetList(args: {
|
|
121
|
+
detail?: string;
|
|
122
|
+
}): Promise<ToolResult>;
|
|
123
|
+
/** Resolve any slot ref (UUID | 8+ hex prefix | `env_…` short code) to the
|
|
124
|
+
* canonical environment row.
|
|
125
|
+
*
|
|
126
|
+
* 🔑 THIS EXISTS BECAUSE THE TWO ROUTES DISAGREE ABOUT REF SHAPES.
|
|
127
|
+
* `GET /environments/{ref}` funnels through `_resolve_env`, which accepts all
|
|
128
|
+
* three. `GET /workstations/{id}` gates on `_is_uuid` and returns 404 for
|
|
129
|
+
* anything else — so an agent that reads `short_id` out of `fleet_list` and
|
|
130
|
+
* hands it to the checklist route gets "not found" for a slot that plainly
|
|
131
|
+
* exists. Resolving here, once, and then using the row's own `id` for the
|
|
132
|
+
* second hop is what keeps `slot_get` honest about which refs it accepts.
|
|
133
|
+
*/
|
|
134
|
+
export declare function resolveSlot(ref: string): Promise<{
|
|
135
|
+
ok: true;
|
|
136
|
+
row: Record<string, unknown>;
|
|
137
|
+
} | {
|
|
138
|
+
ok: false;
|
|
139
|
+
result: ToolResult;
|
|
140
|
+
}>;
|
|
141
|
+
type OnboardingStep = {
|
|
142
|
+
key?: unknown;
|
|
143
|
+
state?: unknown;
|
|
144
|
+
reason?: unknown;
|
|
145
|
+
hint?: unknown;
|
|
146
|
+
title?: unknown;
|
|
147
|
+
};
|
|
148
|
+
/** The one step an operator should act on: the first that is not `done`.
|
|
149
|
+
*
|
|
150
|
+
* Everything after it is `blocked_by:<key>` by construction (OSB-113), so
|
|
151
|
+
* picking the first non-done step is what turns four competing lines into one.
|
|
152
|
+
* Returns null when every step is done — a genuinely ready slot.
|
|
153
|
+
*/
|
|
154
|
+
export declare function nextAction(steps: OnboardingStep[]): Record<string, unknown> | null;
|
|
155
|
+
export declare function handleSlotGet(args: {
|
|
156
|
+
slot: string;
|
|
157
|
+
}): Promise<ToolResult>;
|
|
158
|
+
export declare function handleSlotHealth(args: {
|
|
159
|
+
slot: string;
|
|
160
|
+
}): Promise<ToolResult>;
|
|
161
|
+
export declare function handleFleetLogins(): Promise<ToolResult>;
|
|
162
|
+
export declare const SLOT_RUNTIMES: string[];
|
|
163
|
+
export declare function handleSlotOrder(args: {
|
|
164
|
+
name: string;
|
|
165
|
+
runtime?: string;
|
|
166
|
+
region?: string;
|
|
167
|
+
description?: string;
|
|
168
|
+
}): Promise<ToolResult>;
|
|
169
|
+
export {};
|