attesso 0.4.1 → 0.6.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,171 @@
1
+ // A control-only MCP server. It can inspect and narrow a local configuration;
2
+ // it can never enforce, widen a path scope, or become an enforcement path.
3
+ // Stdio only: the host process owns the filesystem trust.
4
+ import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
5
+ import { homedir } from 'node:os';
6
+ import { join, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { connect, setModeAll, status } from '../connect.mjs';
9
+ import { validatePathsConfig } from '../fs-paths.mjs';
10
+ import { paths } from '../store.mjs';
11
+
12
+ const CLI = fileURLToPath(new URL('../../bin/attesso.mjs', import.meta.url));
13
+
14
+ const TOOLS = [
15
+ {
16
+ name: 'bounds_status',
17
+ description: 'Read Bounds initialization, mode and wired harnesses.',
18
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
19
+ },
20
+ {
21
+ name: 'bounds_connect',
22
+ description: 'Initialize and wire harnesses in observe or off mode. Never enables enforcement.',
23
+ inputSchema: {
24
+ type: 'object',
25
+ properties: { mode: { type: 'string', enum: ['observe', 'off'] } },
26
+ additionalProperties: false,
27
+ },
28
+ },
29
+ {
30
+ name: 'bounds_mode',
31
+ description: 'Set observe or off. Enforce is a human CLI action.',
32
+ inputSchema: {
33
+ type: 'object',
34
+ properties: { mode: { type: 'string', enum: ['observe', 'off'] } },
35
+ required: ['mode'],
36
+ additionalProperties: false,
37
+ },
38
+ },
39
+ {
40
+ name: 'bounds_paths_get',
41
+ description: 'Read the validated file-tool path scope.',
42
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
43
+ },
44
+ {
45
+ name: 'bounds_paths_set',
46
+ description: 'Narrow the path scope. Widening requires the human CLI.',
47
+ inputSchema: {
48
+ type: 'object',
49
+ properties: {
50
+ allowed_roots: { type: 'array' },
51
+ exclusions: { type: 'array' },
52
+ },
53
+ required: ['allowed_roots', 'exclusions'],
54
+ additionalProperties: false,
55
+ },
56
+ },
57
+ ];
58
+
59
+ const readJson = (file) => JSON.parse(readFileSync(file, 'utf8'));
60
+ const writeJson = (file, value) => {
61
+ const temp = `${file}.${process.pid}.tmp`;
62
+ writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
63
+ renameSync(temp, file);
64
+ };
65
+
66
+ function readPaths(dir) {
67
+ if (!existsSync(paths(dir).fsPaths)) return { version: 1, allowed_roots: [], exclusions: [] };
68
+ return validatePathsConfig(readJson(paths(dir).fsPaths));
69
+ }
70
+
71
+ const subset = (next, current, field) => next.every((item) => current.some((existing) => {
72
+ if (field === 'allowed_roots') {
73
+ return existing.path === item.path && item.access.every((access) => existing.access.includes(access));
74
+ }
75
+ return existing === item;
76
+ }));
77
+
78
+ export function createControl({ dir = '.attesso', home = homedir(), cwd = process.cwd() } = {}) {
79
+ const ledger = resolve(dir);
80
+ const statusOf = () => status({ dir: ledger, cwd, home });
81
+
82
+ const call = async (name, args = {}) => {
83
+ if (name === 'bounds_status') return statusOf();
84
+ if (name === 'bounds_connect') {
85
+ const mode = args.mode ?? 'observe';
86
+ if (!['observe', 'off'].includes(mode)) throw new Error('enforce is a human CLI action');
87
+ return { result: connect({ dir: ledger, cwd, home, write: true, mode, cli: CLI }), status: statusOf() };
88
+ }
89
+ if (name === 'bounds_mode') {
90
+ if (!['observe', 'off'].includes(args.mode)) throw new Error('enforce is a human CLI action');
91
+ setModeAll(ledger, args.mode, { home });
92
+ return { mode: args.mode, status: statusOf() };
93
+ }
94
+ if (name === 'bounds_paths_get') return readPaths(ledger);
95
+ if (name === 'bounds_paths_set') {
96
+ const current = readPaths(ledger);
97
+ const next = validatePathsConfig({
98
+ version: 1,
99
+ allowed_roots: args.allowed_roots,
100
+ exclusions: args.exclusions,
101
+ });
102
+ if (!subset(next.allowed_roots, current.allowed_roots, 'allowed_roots')
103
+ || !subset(next.exclusions, current.exclusions, 'exclusions')) {
104
+ throw new Error('path widening requires the human CLI: npx attesso paths --set');
105
+ }
106
+ writeJson(paths(ledger).fsPaths, next);
107
+ return next;
108
+ }
109
+ throw new Error(`unknown tool: ${name}`);
110
+ };
111
+
112
+ return { call, tools: TOOLS, ledger };
113
+ }
114
+
115
+ const result = (id, value) => ({ jsonrpc: '2.0', id, result: value });
116
+ const error = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
117
+ const text = (value) => ({ content: [{ type: 'text', text: JSON.stringify(value) }] });
118
+
119
+ export function createServer({ input = process.stdin, output = process.stdout, control } = {}) {
120
+ let buffer = '';
121
+ let queue = Promise.resolve();
122
+ const send = (message) => output.write(`${JSON.stringify(message)}\n`);
123
+
124
+ const handle = async (message) => {
125
+ if (!message || message.jsonrpc !== '2.0' || typeof message.method !== 'string') {
126
+ return send(error(message?.id ?? null, -32600, 'invalid request'));
127
+ }
128
+ if (Array.isArray(message)) return send(error(null, -32600, 'batch requests are not supported'));
129
+ if (message.method === 'notifications/initialized') return undefined;
130
+ if (message.method === 'initialize') {
131
+ return send(result(message.id, {
132
+ protocolVersion: message.params?.protocolVersion ?? '2025-11-25',
133
+ capabilities: { tools: {} },
134
+ serverInfo: { name: 'attesso-control', version: '0.6.0' },
135
+ }));
136
+ }
137
+ if (message.method === 'ping') return send(result(message.id, {}));
138
+ if (message.method === 'tools/list') return send(result(message.id, { tools: control.tools }));
139
+ if (message.method !== 'tools/call') return send(error(message.id, -32601, `unknown method: ${message.method}`));
140
+ const name = message.params?.name;
141
+ try {
142
+ const value = await control.call(name, message.params?.arguments ?? {});
143
+ return send(result(message.id, text(value)));
144
+ } catch (failure) {
145
+ return send(result(message.id, { content: [{ type: 'text', text: failure.message }], isError: true }));
146
+ }
147
+ };
148
+
149
+ input.setEncoding?.('utf8');
150
+ input.on('data', (chunk) => {
151
+ buffer += chunk;
152
+ let newline;
153
+ while ((newline = buffer.indexOf('\n')) !== -1) {
154
+ const line = buffer.slice(0, newline).trim();
155
+ buffer = buffer.slice(newline + 1);
156
+ if (!line) continue;
157
+ let message;
158
+ try {
159
+ message = JSON.parse(line);
160
+ } catch {
161
+ send(error(null, -32700, 'parse error'));
162
+ continue;
163
+ }
164
+ queue = queue.then(() => handle(message)).catch((failure) => send(error(null, -32603, failure.message)));
165
+ }
166
+ });
167
+ input.on('end', () => { if (buffer.trim()) queue = queue.then(() => send(error(null, -32700, 'parse error'))); });
168
+ return { handle };
169
+ }
170
+
171
+ export { TOOLS };