@orboto/mcp 0.184.1 → 0.185.0
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.
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { expect, it } from 'vitest';
|
|
2
|
+
import { cp, mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
it('real wrapper preserves piped diff text and version-prefixed check/record values', async () => {
|
|
8
|
+
const dir = await mkdtemp(join(tmpdir(), 'orboto-1964-verify-temp-'));
|
|
9
|
+
const fingerprint = 'sha256-diff-v2:' + 'a'.repeat(64);
|
|
10
|
+
const calls = [];
|
|
11
|
+
const server = createServer(async (req, res) => {
|
|
12
|
+
let raw = '';
|
|
13
|
+
for await (const chunk of req)
|
|
14
|
+
raw += chunk.toString();
|
|
15
|
+
const url = req.url ?? '';
|
|
16
|
+
if (req.method === 'POST')
|
|
17
|
+
calls.push({ url, body: JSON.parse(raw) });
|
|
18
|
+
res.setHeader('Content-Type', 'application/json');
|
|
19
|
+
if (url === '/projects')
|
|
20
|
+
res.end(JSON.stringify([{ id: 'p', key: 'ORB' }]));
|
|
21
|
+
else if (url.includes('/tickets/by-key/'))
|
|
22
|
+
res.end(JSON.stringify({ id: 't', projectId: 'p', ticketKey: 'ORB-1964' }));
|
|
23
|
+
else
|
|
24
|
+
res.end(JSON.stringify({ fingerprint, algo: 'sha256-diff-v2' }));
|
|
25
|
+
});
|
|
26
|
+
try {
|
|
27
|
+
await mkdir(join(dir, 'skills/orboto'), { recursive: true });
|
|
28
|
+
await cp(new URL('../../../../skills/orboto/scripts', import.meta.url), join(dir, 'skills/orboto/scripts'), { recursive: true });
|
|
29
|
+
await new Promise((ok) => server.listen(0, '127.0.0.1', ok));
|
|
30
|
+
const address = server.address();
|
|
31
|
+
if (!address || typeof address === 'string')
|
|
32
|
+
throw new Error('Fixture address missing');
|
|
33
|
+
const origin = `http://127.0.0.1:${address.port}`;
|
|
34
|
+
const guard = join(dir, 'network.mjs');
|
|
35
|
+
await writeFile(guard, `const original = globalThis.fetch; globalThis.fetch = (input, init) => { if (new URL(input).origin !== ${JSON.stringify(origin)}) throw new Error('Fixture refused nonlocal request'); return original(input, init); };`);
|
|
36
|
+
const run = (args, stdin = '') => new Promise((ok, no) => {
|
|
37
|
+
const child = spawn(process.execPath, ['--import', guard, join(dir, 'skills/orboto/scripts/orboto.mjs'), ...args], {
|
|
38
|
+
cwd: dir, env: { ...process.env, ORBOTO_PROFILE: '', ORBOTO_BASE_URL: origin, ORBOTO_TOKEN: 'orb_fixture', ORBOTO_DAEMON: '', ORBOTO_AGENT_SESSION: 'fixture' },
|
|
39
|
+
timeout: 20_000,
|
|
40
|
+
});
|
|
41
|
+
let out = '', err = '';
|
|
42
|
+
child.stdout.on('data', (chunk) => { out += chunk.toString(); });
|
|
43
|
+
child.stderr.on('data', (chunk) => { err += chunk.toString(); });
|
|
44
|
+
child.on('error', no);
|
|
45
|
+
child.on('close', (code) => code === 0 ? ok(out) : no(new Error(`Wrapper exit ${code}: ${err}`)));
|
|
46
|
+
child.stdin.end(stdin);
|
|
47
|
+
});
|
|
48
|
+
const diff = 'diff --git a/a b/a\r\n-old\r\n+ new \r\n+\r\n';
|
|
49
|
+
expect(await run(['review-fingerprint'], diff)).toContain(fingerprint);
|
|
50
|
+
expect(calls[0]).toEqual({ url: '/review-policy/fingerprint', body: { diff } });
|
|
51
|
+
expect(await run(['review-check', 'ORB-1964', '--fingerprint', fingerprint])).toContain(fingerprint);
|
|
52
|
+
expect(await run(['review-approve', 'ORB-1964', '--fingerprint', fingerprint])).toContain(fingerprint);
|
|
53
|
+
expect(calls.slice(1).map((call) => call.body.fingerprint)).toEqual([fingerprint, fingerprint]);
|
|
54
|
+
expect(calls[2].body.decision).toBe('approved');
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
server.closeAllConnections();
|
|
58
|
+
if (server.listening)
|
|
59
|
+
await new Promise((ok, no) => server.close((err) => err ? no(err) : ok()));
|
|
60
|
+
await rm(dir, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
}, 60_000);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ORB-1615 - structural review policy tools.
|
|
3
3
|
*
|
|
4
|
-
* - orboto_review_fingerprint -
|
|
4
|
+
* - orboto_review_fingerprint - hash exact raw diff text into a
|
|
5
5
|
* canonical fingerprint (+ real size
|
|
6
6
|
* metrics), server-side (one algorithm,
|
|
7
7
|
* not one per client).
|
|
@@ -23,7 +23,7 @@ import { resolveTicketByKey } from './shared.js';
|
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
24
|
export const reviewFingerprintToolConfig = {
|
|
25
25
|
title: 'Compute a canonical diff fingerprint',
|
|
26
|
-
description: '
|
|
26
|
+
description: 'Hash the exact submitted diff text server-side with sha256-diff-v2. Every text change, including indentation, blank lines, binary blob identities, mode/rename metadata and file order, changes the fingerprint. This is submitted-text identity, NOT independent repository attestation. Returns size/path metrics too. Preserve the complete opaque version-prefixed `fingerprint` for `orboto_review_policy_check` and `orboto_review_approval_record`; legacy v1 approvals remain history only and must be reviewed again.',
|
|
27
27
|
inputSchema: z.object({
|
|
28
28
|
diff: z.string().min(1).describe('Raw unified diff text, e.g. the output of `git diff` / `git diff --no-color`.'),
|
|
29
29
|
}).shape,
|
|
@@ -76,7 +76,7 @@ export function makeReviewPolicyCheckHandler(client) {
|
|
|
76
76
|
// ---------------------------------------------------------------------------
|
|
77
77
|
export const reviewApprovalRecordToolConfig = {
|
|
78
78
|
title: 'Record a review decision against a diff fingerprint',
|
|
79
|
-
description: 'Record your review verdict (approve/reject) against
|
|
79
|
+
description: 'Record your review verdict (approve/reject) against the complete version-prefixed fingerprint from orboto_review_fingerprint. A current-algorithm approval is reusable only for identical submitted diff text; even whitespace or file-order changes require a fresh review. Legacy/unversioned records remain history only. This advisory review ledger does not itself enforce ticket closure. Requires ticket:record_review_approval.',
|
|
80
80
|
inputSchema: z.object({
|
|
81
81
|
ticketKey: z.string().min(3).describe('Ticket key, e.g. ORB-42.'),
|
|
82
82
|
fingerprint: z.string().min(8).describe('From orboto_review_fingerprint.'),
|
|
@@ -21,14 +21,17 @@ function stub(responses) {
|
|
|
21
21
|
const client = new OrbotoClient({ baseUrl: 'https://orboto.example.com', apiKey: 'orb_x' });
|
|
22
22
|
const PROJ = { id: 'p1', key: 'ACME', name: 'Acme', description: null, status: 'active' };
|
|
23
23
|
const TICKET = { id: 't1', projectId: 'p1', ticketKey: 'ACME-1', ticketNumber: 1, title: 'Bug', description: null, status: 'IN_PROGRESS', statusName: 'In Progress', statusCategory: 'in_progress', type: 'bug', priority: 'normal', estimatedTimeMinutes: 0, dueDate: null, isPrivate: false };
|
|
24
|
+
const FINGERPRINT = 'sha256-diff-v2:' + 'a'.repeat(64);
|
|
24
25
|
describe('orboto_review_fingerprint', () => {
|
|
25
26
|
it('computes a fingerprint + size metrics from raw diff text', async () => {
|
|
26
|
-
const calls = stub([{ json: { fingerprint:
|
|
27
|
-
const
|
|
27
|
+
const calls = stub([{ json: { fingerprint: FINGERPRINT, algo: 'sha256-diff-v2', filesChanged: 1, linesAdded: 1, linesRemoved: 1, paths: ['src/foo.ts'] } }]);
|
|
28
|
+
const diff = 'diff --git a/src/foo.ts b/src/foo.ts\r\n-a\r\n+ b \r\n+\r\n';
|
|
29
|
+
const res = await makeReviewFingerprintHandler(client)({ diff });
|
|
28
30
|
expect(calls[0].url).toContain('/review-policy/fingerprint');
|
|
29
31
|
expect(calls[0].method).toBe('POST');
|
|
30
|
-
expect(
|
|
31
|
-
expect(res.
|
|
32
|
+
expect(calls[0].body).toEqual({ diff });
|
|
33
|
+
expect(res.structuredContent).toMatchObject({ fingerprint: FINGERPRINT, algo: 'sha256-diff-v2' });
|
|
34
|
+
expect(res.content[0].text).toContain(FINGERPRINT);
|
|
32
35
|
});
|
|
33
36
|
});
|
|
34
37
|
describe('orboto_review_policy_check', () => {
|
|
@@ -38,10 +41,10 @@ describe('orboto_review_policy_check', () => {
|
|
|
38
41
|
fingerprintChecked: true, hasValidApproval: false, latestApproval: null,
|
|
39
42
|
};
|
|
40
43
|
const calls = stub([{ json: PROJ }, { json: TICKET }, { json: decision }]);
|
|
41
|
-
const res = await makeReviewPolicyCheckHandler(client)({ ticketKey: 'ACME-1', fingerprint:
|
|
44
|
+
const res = await makeReviewPolicyCheckHandler(client)({ ticketKey: 'ACME-1', fingerprint: FINGERPRINT });
|
|
42
45
|
const check = calls.find((c) => c.method === 'POST');
|
|
43
46
|
expect(check?.url).toContain('/projects/p1/tickets/t1/review-policy/check');
|
|
44
|
-
expect(check?.body).toMatchObject({ fingerprint:
|
|
47
|
+
expect(check?.body).toMatchObject({ fingerprint: FINGERPRINT });
|
|
45
48
|
expect(res.structuredContent).toMatchObject({ riskLevel: 'required', hasValidApproval: false });
|
|
46
49
|
expect(res.content[0].text).toContain('required');
|
|
47
50
|
expect(res.content[0].text).toContain('No valid approval');
|
|
@@ -58,11 +61,11 @@ describe('orboto_review_policy_check', () => {
|
|
|
58
61
|
});
|
|
59
62
|
describe('orboto_review_approval_record', () => {
|
|
60
63
|
it('records an approval against a fingerprint', async () => {
|
|
61
|
-
const approval = { id: 'ra1', ticketId: 't1', fingerprint:
|
|
64
|
+
const approval = { id: 'ra1', ticketId: 't1', fingerprint: FINGERPRINT, decision: 'approved', reviewerLabel: 'reviewer@orboto.test', note: null, revokedAt: null, createdAt: '2026-01-01T00:00:00Z' };
|
|
62
65
|
const calls = stub([{ json: PROJ }, { json: TICKET }, { json: approval }]);
|
|
63
|
-
const res = await makeReviewApprovalRecordHandler(client)({ ticketKey: 'ACME-1', fingerprint:
|
|
66
|
+
const res = await makeReviewApprovalRecordHandler(client)({ ticketKey: 'ACME-1', fingerprint: FINGERPRINT, decision: 'approved', note: 'looks good' });
|
|
64
67
|
const record = calls.find((c) => c.method === 'POST' && c.url.includes('review-approvals'));
|
|
65
|
-
expect(record?.body).toMatchObject({ fingerprint:
|
|
68
|
+
expect(record?.body).toMatchObject({ fingerprint: FINGERPRINT, decision: 'approved', note: 'looks good' });
|
|
66
69
|
// ticketKey must NOT leak into the request body sent to the API - the
|
|
67
70
|
// route resolves the ticket from the URL, not the payload.
|
|
68
71
|
expect(record?.body).not.toHaveProperty('ticketKey');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orboto/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.185.0",
|
|
4
4
|
"description": "Model Context Protocol server for orboto - exposes tickets, projects, milestones, time, docs, primer-facts, alerts and absences as MCP tools for Claude Desktop / Cursor / Copilot Chat / any MCP-aware AI client.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"vitest": "^1.6.1"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
|
-
"node": "
|
|
40
|
+
"node": "^22.20.0 || ^24.0.0"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"orboto",
|