hadara 0.1.0-rc.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.
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/dist/agent/evidence.js +50 -0
- package/dist/agent/loop.js +124 -0
- package/dist/cli/args.js +70 -0
- package/dist/cli/dashboard.js +185 -0
- package/dist/cli/debt.js +41 -0
- package/dist/cli/doctor.js +68 -0
- package/dist/cli/errors.js +58 -0
- package/dist/cli/evidence-json.js +75 -0
- package/dist/cli/evidence.js +80 -0
- package/dist/cli/handoff.js +16 -0
- package/dist/cli/harness.js +57 -0
- package/dist/cli/hermes-json.js +31 -0
- package/dist/cli/hermes.js +28 -0
- package/dist/cli/init.js +142 -0
- package/dist/cli/install.js +34 -0
- package/dist/cli/main.js +216 -0
- package/dist/cli/mcp.js +15 -0
- package/dist/cli/package-smoke.js +37 -0
- package/dist/cli/policy-json.js +22 -0
- package/dist/cli/policy.js +43 -0
- package/dist/cli/release-artifact.js +47 -0
- package/dist/cli/release-dry-run.js +24 -0
- package/dist/cli/release-gate.js +28 -0
- package/dist/cli/release-publish.js +41 -0
- package/dist/cli/run-scaffold.js +68 -0
- package/dist/cli/run-state.js +41 -0
- package/dist/cli/run.js +191 -0
- package/dist/cli/smoke.js +58 -0
- package/dist/cli/status-json.js +6 -0
- package/dist/cli/status.js +26 -0
- package/dist/cli/task-json.js +8 -0
- package/dist/cli/task.js +64 -0
- package/dist/cli/tools.js +25 -0
- package/dist/cli/tui.js +72 -0
- package/dist/cli/write-preflight.js +27 -0
- package/dist/core/audit.js +41 -0
- package/dist/core/events.js +63 -0
- package/dist/core/fs.js +44 -0
- package/dist/core/paths.js +59 -0
- package/dist/core/redaction.js +178 -0
- package/dist/core/schema.js +253 -0
- package/dist/core/workspace.js +47 -0
- package/dist/evidence/evidence.js +170 -0
- package/dist/evidence/private-manifest.js +101 -0
- package/dist/handoff/handoff.js +49 -0
- package/dist/harness/replay.js +200 -0
- package/dist/harness/validate.js +465 -0
- package/dist/hermes/context-export.js +104 -0
- package/dist/index.js +29 -0
- package/dist/mcp/server.js +104 -0
- package/dist/mcp/tool-dispatch.js +159 -0
- package/dist/mcp/tool-registry.js +150 -0
- package/dist/mcp/tool-schemas.js +18 -0
- package/dist/policy/command-risk.js +39 -0
- package/dist/policy/permission-matrix.js +42 -0
- package/dist/policy/policy.js +20 -0
- package/dist/policy/preflight.js +47 -0
- package/dist/policy/presets.js +24 -0
- package/dist/policy/tokenizer.js +53 -0
- package/dist/providers/fallback-executor.js +46 -0
- package/dist/providers/mock-provider.js +49 -0
- package/dist/providers/provider-contract.js +2 -0
- package/dist/providers/provider-preparation.js +220 -0
- package/dist/providers/scripted-provider.js +69 -0
- package/dist/schemas/active-run-projection.schema.json +73 -0
- package/dist/schemas/active-run-resume.schema.json +68 -0
- package/dist/schemas/clean-checkout-smoke.schema.json +126 -0
- package/dist/schemas/context-export.schema.json +35 -0
- package/dist/schemas/event.schema.json +17 -0
- package/dist/schemas/evidence-list.schema.json +49 -0
- package/dist/schemas/feature-smoke.schema.json +67 -0
- package/dist/schemas/install-plan.schema.json +93 -0
- package/dist/schemas/package-smoke.schema.json +130 -0
- package/dist/schemas/private-evidence.schema.json +48 -0
- package/dist/schemas/provider-call.schema.json +42 -0
- package/dist/schemas/provider-config.schema.json +43 -0
- package/dist/schemas/release-artifact-manifest.schema.json +55 -0
- package/dist/schemas/release-artifact.schema.json +140 -0
- package/dist/schemas/release-dry-run.schema.json +141 -0
- package/dist/schemas/release-gate.schema.json +42 -0
- package/dist/schemas/release-publish.schema.json +114 -0
- package/dist/schemas/schema-index.json +145 -0
- package/dist/schemas/smoke-evidence-summary.schema.json +88 -0
- package/dist/schemas/tools-list.schema.json +78 -0
- package/dist/schemas/write-preflight.schema.json +47 -0
- package/dist/services/active-run-state.js +215 -0
- package/dist/services/capability-registry.js +540 -0
- package/dist/services/clean-checkout-smoke.js +393 -0
- package/dist/services/evidence-list.js +136 -0
- package/dist/services/feature-smoke.js +155 -0
- package/dist/services/harness-service.js +7 -0
- package/dist/services/install-plan.js +233 -0
- package/dist/services/operational-debt.js +767 -0
- package/dist/services/operations-status-service.js +195 -0
- package/dist/services/package-smoke.js +676 -0
- package/dist/services/policy-service.js +25 -0
- package/dist/services/project-read-model.js +101 -0
- package/dist/services/release-artifact-evidence.js +77 -0
- package/dist/services/release-artifact.js +351 -0
- package/dist/services/release-dry-run.js +253 -0
- package/dist/services/release-evidence.js +138 -0
- package/dist/services/release-publish.js +163 -0
- package/dist/services/smoke-evidence.js +104 -0
- package/dist/services/task-read-model.js +125 -0
- package/dist/services/tools-list.js +26 -0
- package/dist/services/write-preflight.js +240 -0
- package/dist/task/task-capsule.js +121 -0
- package/dist/tools/fake-shell.js +56 -0
- package/dist/tui/cache.js +341 -0
- package/dist/tui/constants.js +44 -0
- package/dist/tui/layout.js +140 -0
- package/dist/tui/markdown.js +238 -0
- package/dist/tui/read-model-worker.js +24 -0
- package/dist/tui/read-model.js +502 -0
- package/dist/tui/snapshot.js +434 -0
- package/dist/tui/state.js +229 -0
- package/dist/tui/terminal.js +475 -0
- package/dist/tui/theme.js +86 -0
- package/package.json +16 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.handleMcpJsonRpcMessage = handleMcpJsonRpcMessage;
|
|
7
|
+
exports.startMcpStdioServer = startMcpStdioServer;
|
|
8
|
+
const node_readline_1 = __importDefault(require("node:readline"));
|
|
9
|
+
const tool_dispatch_1 = require("./tool-dispatch");
|
|
10
|
+
const tool_registry_1 = require("./tool-registry");
|
|
11
|
+
function handleMcpJsonRpcMessage(message, options = {}) {
|
|
12
|
+
let parsed;
|
|
13
|
+
try {
|
|
14
|
+
parsed = JSON.parse(message);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return serializeError(null, -32700, 'Parse error');
|
|
18
|
+
}
|
|
19
|
+
if (!isJsonRpcRequest(parsed)) {
|
|
20
|
+
return serializeError(null, -32600, 'Invalid Request');
|
|
21
|
+
}
|
|
22
|
+
if (parsed.id === undefined) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return JSON.stringify(handleMcpRequest(parsed, options));
|
|
26
|
+
}
|
|
27
|
+
function startMcpStdioServer(options = {}, input = process.stdin, output = process.stdout) {
|
|
28
|
+
const lines = node_readline_1.default.createInterface({ input });
|
|
29
|
+
lines.on('line', (line) => {
|
|
30
|
+
const trimmed = line.trim();
|
|
31
|
+
if (!trimmed)
|
|
32
|
+
return;
|
|
33
|
+
const response = handleMcpJsonRpcMessage(trimmed, options);
|
|
34
|
+
if (response)
|
|
35
|
+
output.write(`${response}\n`);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function handleMcpRequest(request, options) {
|
|
39
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
40
|
+
const tools = (0, tool_registry_1.createMcpToolRegistry)(projectRoot, { enableEvidenceAttach: options.enableEvidenceAttach });
|
|
41
|
+
const writeEnabled = options.enableEvidenceAttach === true;
|
|
42
|
+
switch (request.method) {
|
|
43
|
+
case 'initialize':
|
|
44
|
+
return success(request.id ?? null, {
|
|
45
|
+
protocolVersion: '2024-11-05',
|
|
46
|
+
serverInfo: {
|
|
47
|
+
name: 'hadara',
|
|
48
|
+
version: '0.0.0-bootstrap'
|
|
49
|
+
},
|
|
50
|
+
capabilities: {
|
|
51
|
+
tools: {
|
|
52
|
+
listChanged: false
|
|
53
|
+
},
|
|
54
|
+
_meta: {
|
|
55
|
+
'hadara/phase': writeEnabled ? 'evidence-attach-enabled' : 'read-only-bridge',
|
|
56
|
+
'hadara/readOnly': !writeEnabled,
|
|
57
|
+
'hadara/writes': writeEnabled,
|
|
58
|
+
'hadara/evidenceAttach': writeEnabled,
|
|
59
|
+
'hadara/shellExecution': false,
|
|
60
|
+
'hadara/providerCalls': false
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
instructions: writeEnabled
|
|
64
|
+
? 'HADARA MCP evidence attach is enabled only for this server process. Evidence writes require per-call approval metadata, are audited privately, and never execute shell commands or call providers.'
|
|
65
|
+
: 'HADARA MCP is running in default read-only mode. Evidence attach, shell execution, and provider calls are disabled for this server process.'
|
|
66
|
+
});
|
|
67
|
+
case 'tools/list':
|
|
68
|
+
return success(request.id ?? null, { tools: tools.map((tool) => tool.metadata) });
|
|
69
|
+
case 'tools/call': {
|
|
70
|
+
try {
|
|
71
|
+
return success(request.id ?? null, (0, tool_dispatch_1.dispatchMcpToolCall)(request.params, tools));
|
|
72
|
+
}
|
|
73
|
+
catch (toolError) {
|
|
74
|
+
if (toolError instanceof tool_dispatch_1.McpToolDispatchError) {
|
|
75
|
+
return error(request.id ?? null, toolError.jsonRpcCode, toolError.message, { issue: toolError.issue });
|
|
76
|
+
}
|
|
77
|
+
return error(request.id ?? null, -32603, toolError instanceof Error ? toolError.message : String(toolError));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
default:
|
|
81
|
+
return error(request.id ?? null, -32601, `Method not found: ${request.method}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function isJsonRpcRequest(value) {
|
|
85
|
+
if (typeof value !== 'object' || value === null)
|
|
86
|
+
return false;
|
|
87
|
+
const candidate = value;
|
|
88
|
+
if (candidate.jsonrpc !== '2.0')
|
|
89
|
+
return false;
|
|
90
|
+
if (typeof candidate.method !== 'string')
|
|
91
|
+
return false;
|
|
92
|
+
if (candidate.id !== undefined && candidate.id !== null && typeof candidate.id !== 'string' && typeof candidate.id !== 'number')
|
|
93
|
+
return false;
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
function success(id, result) {
|
|
97
|
+
return { jsonrpc: '2.0', id, result };
|
|
98
|
+
}
|
|
99
|
+
function error(id, code, message, data) {
|
|
100
|
+
return { jsonrpc: '2.0', id, error: { code, message, ...(data === undefined ? {} : { data }) } };
|
|
101
|
+
}
|
|
102
|
+
function serializeError(id, code, message) {
|
|
103
|
+
return JSON.stringify(error(id, code, message));
|
|
104
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.McpToolDispatchError = exports.MCP_TOOL_ISSUE_CODES = void 0;
|
|
4
|
+
exports.dispatchMcpToolCall = dispatchMcpToolCall;
|
|
5
|
+
exports.wrapJsonTextPayload = wrapJsonTextPayload;
|
|
6
|
+
exports.MCP_TOOL_ISSUE_CODES = [
|
|
7
|
+
'TOOL_NOT_FOUND',
|
|
8
|
+
'TOOL_INPUT_INVALID',
|
|
9
|
+
'TOOL_NOT_IMPLEMENTED',
|
|
10
|
+
'TOOL_FORBIDDEN_BY_PHASE',
|
|
11
|
+
'TOOL_POLICY_DENIED',
|
|
12
|
+
'TOOL_WRITE_FORBIDDEN',
|
|
13
|
+
'TOOL_WORKSPACE_BOUNDARY',
|
|
14
|
+
'TOOL_ARTIFACT_REDACTION_FAILED',
|
|
15
|
+
'TOOL_SCHEMA_VERSION_MISMATCH'
|
|
16
|
+
];
|
|
17
|
+
class McpToolDispatchError extends Error {
|
|
18
|
+
issue;
|
|
19
|
+
jsonRpcCode;
|
|
20
|
+
constructor(issue, jsonRpcCode = -32602) {
|
|
21
|
+
super(issue.message);
|
|
22
|
+
this.issue = issue;
|
|
23
|
+
this.jsonRpcCode = jsonRpcCode;
|
|
24
|
+
this.name = 'McpToolDispatchError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.McpToolDispatchError = McpToolDispatchError;
|
|
28
|
+
function dispatchMcpToolCall(params, tools) {
|
|
29
|
+
const call = parseToolCallParams(params);
|
|
30
|
+
const tool = tools.find((item) => item.metadata.name === call.name);
|
|
31
|
+
if (!tool) {
|
|
32
|
+
throw new McpToolDispatchError({
|
|
33
|
+
severity: 'error',
|
|
34
|
+
code: 'TOOL_NOT_FOUND',
|
|
35
|
+
message: `MCP tool not found: ${call.name}`
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (!tool.phaseAllowed) {
|
|
39
|
+
throw new McpToolDispatchError({
|
|
40
|
+
severity: 'error',
|
|
41
|
+
code: 'TOOL_FORBIDDEN_BY_PHASE',
|
|
42
|
+
message: `MCP tool is forbidden in the current HADARA phase: ${call.name}`
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (!tool.handler) {
|
|
46
|
+
throw new McpToolDispatchError({
|
|
47
|
+
severity: 'error',
|
|
48
|
+
code: 'TOOL_NOT_IMPLEMENTED',
|
|
49
|
+
message: `MCP tool is not implemented: ${call.name}`
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
validateToolArguments(call.name, call.arguments, tool.metadata.inputSchema);
|
|
53
|
+
return wrapJsonTextPayload(tool.handler(call.arguments));
|
|
54
|
+
}
|
|
55
|
+
function wrapJsonTextPayload(report) {
|
|
56
|
+
return {
|
|
57
|
+
content: [
|
|
58
|
+
{
|
|
59
|
+
type: 'text',
|
|
60
|
+
text: JSON.stringify(report)
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function parseToolCallParams(params) {
|
|
66
|
+
if (!isPlainObject(params)) {
|
|
67
|
+
throw invalidInput('tools/call params must be an object.');
|
|
68
|
+
}
|
|
69
|
+
if (typeof params.name !== 'string' || params.name.trim().length === 0) {
|
|
70
|
+
throw invalidInput('tools/call params.name must be a non-empty string.');
|
|
71
|
+
}
|
|
72
|
+
if (!isPlainObject(params.arguments)) {
|
|
73
|
+
throw invalidInput('tools/call params.arguments must be an object.');
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
name: params.name,
|
|
77
|
+
arguments: params.arguments
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function validateToolArguments(toolName, args, schema) {
|
|
81
|
+
const required = schema.required ?? [];
|
|
82
|
+
for (const name of required) {
|
|
83
|
+
if (!(name in args)) {
|
|
84
|
+
throw invalidInput(`${toolName} requires argument: ${name}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!schema.additionalProperties) {
|
|
88
|
+
for (const name of Object.keys(args)) {
|
|
89
|
+
if (!(name in schema.properties)) {
|
|
90
|
+
throw invalidInput(`${toolName} does not accept argument: ${name}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
validateSchemaProperties(toolName, args, schema.properties, schema.required ?? []);
|
|
95
|
+
}
|
|
96
|
+
function validateSchemaProperties(toolName, args, properties, required, pathPrefix = '') {
|
|
97
|
+
for (const name of required) {
|
|
98
|
+
if (!(name in args)) {
|
|
99
|
+
throw invalidInput(`${toolName} requires argument: ${pathPrefix}${name}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const [name, property] of Object.entries(properties)) {
|
|
103
|
+
const value = args[name];
|
|
104
|
+
if (value === undefined)
|
|
105
|
+
continue;
|
|
106
|
+
const argumentName = `${pathPrefix}${name}`;
|
|
107
|
+
if (property.type === 'boolean' && typeof value !== 'boolean') {
|
|
108
|
+
throw invalidInput(`${toolName} argument ${argumentName} must be a boolean.`);
|
|
109
|
+
}
|
|
110
|
+
if (property.type === 'integer') {
|
|
111
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
|
112
|
+
throw invalidInput(`${toolName} argument ${argumentName} must be an integer.`);
|
|
113
|
+
}
|
|
114
|
+
if (property.minimum !== undefined && value < property.minimum) {
|
|
115
|
+
throw invalidInput(`${toolName} argument ${argumentName} must be greater than or equal to ${property.minimum}.`);
|
|
116
|
+
}
|
|
117
|
+
if (property.maximum !== undefined && value > property.maximum) {
|
|
118
|
+
throw invalidInput(`${toolName} argument ${argumentName} must be less than or equal to ${property.maximum}.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (property.type === 'string') {
|
|
122
|
+
if (typeof value !== 'string') {
|
|
123
|
+
throw invalidInput(`${toolName} argument ${argumentName} must be a string.`);
|
|
124
|
+
}
|
|
125
|
+
if (property.minLength !== undefined && value.length < property.minLength) {
|
|
126
|
+
throw invalidInput(`${toolName} argument ${argumentName} must be at least ${property.minLength} character(s).`);
|
|
127
|
+
}
|
|
128
|
+
if (property.pattern !== undefined && !new RegExp(property.pattern).test(value)) {
|
|
129
|
+
throw invalidInput(`${toolName} argument ${argumentName} must match ${property.pattern}.`);
|
|
130
|
+
}
|
|
131
|
+
if (property.enum !== undefined && !property.enum.includes(value)) {
|
|
132
|
+
throw invalidInput(`${toolName} argument ${argumentName} must be one of: ${property.enum.join(', ')}.`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (property.type === 'object') {
|
|
136
|
+
if (!isPlainObject(value)) {
|
|
137
|
+
throw invalidInput(`${toolName} argument ${argumentName} must be an object.`);
|
|
138
|
+
}
|
|
139
|
+
if (!property.additionalProperties) {
|
|
140
|
+
for (const childName of Object.keys(value)) {
|
|
141
|
+
if (!(childName in property.properties)) {
|
|
142
|
+
throw invalidInput(`${toolName} argument ${argumentName} does not accept property: ${childName}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
validateSchemaProperties(toolName, value, property.properties, property.required ?? [], `${argumentName}.`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function invalidInput(message) {
|
|
151
|
+
return new McpToolDispatchError({
|
|
152
|
+
severity: 'error',
|
|
153
|
+
code: 'TOOL_INPUT_INVALID',
|
|
154
|
+
message
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function isPlainObject(value) {
|
|
158
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
159
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createMcpToolRegistry = createMcpToolRegistry;
|
|
4
|
+
const evidence_json_1 = require("../cli/evidence-json");
|
|
5
|
+
const audit_1 = require("../core/audit");
|
|
6
|
+
const paths_1 = require("../core/paths");
|
|
7
|
+
const context_export_1 = require("../hermes/context-export");
|
|
8
|
+
const project_read_model_1 = require("../services/project-read-model");
|
|
9
|
+
const active_run_state_1 = require("../services/active-run-state");
|
|
10
|
+
const evidence_list_1 = require("../services/evidence-list");
|
|
11
|
+
const harness_service_1 = require("../services/harness-service");
|
|
12
|
+
const operational_debt_1 = require("../services/operational-debt");
|
|
13
|
+
const policy_service_1 = require("../services/policy-service");
|
|
14
|
+
const task_read_model_1 = require("../services/task-read-model");
|
|
15
|
+
const tools_list_1 = require("../services/tools-list");
|
|
16
|
+
const tool_schemas_1 = require("./tool-schemas");
|
|
17
|
+
function createMcpToolRegistry(projectRoot, options = {}) {
|
|
18
|
+
const readTools = tool_schemas_1.HADARA_MCP_TOOL_SCHEMAS.map((metadata) => ({
|
|
19
|
+
metadata,
|
|
20
|
+
phaseAllowed: true,
|
|
21
|
+
handler: (args) => handleReadOnlyTool(projectRoot, metadata.name, args, options)
|
|
22
|
+
}));
|
|
23
|
+
if (!options.enableEvidenceAttach)
|
|
24
|
+
return readTools;
|
|
25
|
+
return [
|
|
26
|
+
...readTools,
|
|
27
|
+
{
|
|
28
|
+
metadata: tool_schemas_1.HADARA_MCP_EVIDENCE_ATTACH_SCHEMA,
|
|
29
|
+
phaseAllowed: true,
|
|
30
|
+
handler: (args) => handleEvidenceAttachTool(projectRoot, args)
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
function handleReadOnlyTool(projectRoot, name, args, options) {
|
|
35
|
+
switch (name) {
|
|
36
|
+
case 'hadara.task.list':
|
|
37
|
+
return {
|
|
38
|
+
...(0, task_read_model_1.createTaskListReport)(projectRoot),
|
|
39
|
+
issues: []
|
|
40
|
+
};
|
|
41
|
+
case 'hadara.task.read':
|
|
42
|
+
return (0, task_read_model_1.createTaskReadReport)(projectRoot, String(args.taskId), {
|
|
43
|
+
includePrivate: args.includePrivate === true
|
|
44
|
+
});
|
|
45
|
+
case 'hadara.handoff.read':
|
|
46
|
+
return (0, project_read_model_1.createHandoffReadReport)(projectRoot, {
|
|
47
|
+
includeHistory: args.includeHistory === true,
|
|
48
|
+
historyLimit: typeof args.historyLimit === 'number' ? args.historyLimit : 20
|
|
49
|
+
});
|
|
50
|
+
case 'hadara.project.state.read':
|
|
51
|
+
return (0, project_read_model_1.createProjectStateReadReport)(projectRoot, {
|
|
52
|
+
includeDocuments: args.includeDocuments === undefined ? true : args.includeDocuments === true,
|
|
53
|
+
summaryOnly: args.summaryOnly === true
|
|
54
|
+
});
|
|
55
|
+
case 'hadara.policy.evaluate':
|
|
56
|
+
return (0, policy_service_1.createPolicyEvaluateReport)(String(args.command), typeof args.mode === 'string' ? args.mode : 'assisted');
|
|
57
|
+
case 'hadara.harness.validate':
|
|
58
|
+
return (0, harness_service_1.createHarnessValidateReport)(projectRoot, String(args.taskId), {
|
|
59
|
+
level: args.level === 'done' ? 'done' : 'draft'
|
|
60
|
+
});
|
|
61
|
+
case 'hadara.evidence.list':
|
|
62
|
+
return (0, evidence_list_1.createEvidenceListReport)(projectRoot, {
|
|
63
|
+
taskId: String(args.taskId),
|
|
64
|
+
limit: typeof args.limit === 'number' ? args.limit : undefined,
|
|
65
|
+
includePrivate: args.includePrivate === true
|
|
66
|
+
});
|
|
67
|
+
case 'hadara.context.export':
|
|
68
|
+
return (0, context_export_1.createContextExportReport)(projectRoot, {
|
|
69
|
+
format: args.format === 'json' ? 'json' : 'markdown',
|
|
70
|
+
summaryOnly: args.summaryOnly === true
|
|
71
|
+
});
|
|
72
|
+
case 'hadara.tools.list':
|
|
73
|
+
return (0, tools_list_1.createToolsListReport)({ enableEvidenceAttach: options.enableEvidenceAttach });
|
|
74
|
+
case 'hadara.active.run.read':
|
|
75
|
+
return (0, active_run_state_1.safeCreateActiveRunProjection)(projectRoot);
|
|
76
|
+
case 'hadara.active.run.resume':
|
|
77
|
+
return (0, active_run_state_1.createActiveRunResumeReport)(projectRoot);
|
|
78
|
+
case 'hadara.debt.list':
|
|
79
|
+
return (0, operational_debt_1.createOperationalDebtReport)(projectRoot);
|
|
80
|
+
case 'hadara.debt.show':
|
|
81
|
+
return (0, operational_debt_1.createOperationalDebtShowReport)(projectRoot, String(args.id));
|
|
82
|
+
default:
|
|
83
|
+
throw new Error(`unregistered MCP tool handler: ${name}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function handleEvidenceAttachTool(projectRoot, args) {
|
|
87
|
+
const approval = parseApproval(args.approval);
|
|
88
|
+
const report = (0, evidence_json_1.createEvidenceCollectReport)(projectRoot, {
|
|
89
|
+
taskId: String(args.taskId),
|
|
90
|
+
kind: parseEvidenceKind(String(args.kind)),
|
|
91
|
+
summary: String(args.summary),
|
|
92
|
+
result: parseEvidenceResult(String(args.result)),
|
|
93
|
+
visibility: parseEvidenceVisibility(typeof args.visibility === 'string' ? args.visibility : 'public'),
|
|
94
|
+
path: typeof args.artifactPath === 'string' ? args.artifactPath : undefined
|
|
95
|
+
});
|
|
96
|
+
auditEvidenceAttach(projectRoot, args, approval, report);
|
|
97
|
+
return report;
|
|
98
|
+
}
|
|
99
|
+
function parseApproval(value) {
|
|
100
|
+
const approval = value;
|
|
101
|
+
return {
|
|
102
|
+
actor: String(approval.actor),
|
|
103
|
+
reason: String(approval.reason)
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function auditEvidenceAttach(projectRoot, args, approval, report) {
|
|
107
|
+
const result = isEvidenceCollectReport(report) && report.ok ? 'succeeded' : 'failed';
|
|
108
|
+
const issues = isEvidenceCollectReport(report) ? report.issues.map((issue) => ({ code: issue.code, severity: issue.severity })) : [];
|
|
109
|
+
(0, audit_1.writeAuditEvent)((0, paths_1.resolveHadaraPaths)({ projectRoot }).auditDir, {
|
|
110
|
+
actor: 'agent',
|
|
111
|
+
task_id: typeof args.taskId === 'string' ? args.taskId : undefined,
|
|
112
|
+
event_type: `mcp.evidence.attach.${result}`,
|
|
113
|
+
risk: result === 'succeeded' ? 'medium' : 'blocked',
|
|
114
|
+
summary: `MCP evidence attach ${result} for ${String(args.taskId)}`,
|
|
115
|
+
payload: {
|
|
116
|
+
tool: 'hadara.evidence.attach',
|
|
117
|
+
approval,
|
|
118
|
+
input: {
|
|
119
|
+
taskId: args.taskId,
|
|
120
|
+
kind: args.kind,
|
|
121
|
+
result: args.result,
|
|
122
|
+
visibility: typeof args.visibility === 'string' ? args.visibility : 'public',
|
|
123
|
+
artifactPathProvided: typeof args.artifactPath === 'string'
|
|
124
|
+
},
|
|
125
|
+
ok: result === 'succeeded',
|
|
126
|
+
issues
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function isEvidenceCollectReport(value) {
|
|
131
|
+
if (typeof value !== 'object' || value === null)
|
|
132
|
+
return false;
|
|
133
|
+
const candidate = value;
|
|
134
|
+
return typeof candidate.ok === 'boolean' && Array.isArray(candidate.issues);
|
|
135
|
+
}
|
|
136
|
+
function parseEvidenceKind(value) {
|
|
137
|
+
if (value === 'test-log' || value === 'command-log' || value === 'diff-summary' || value === 'screenshot' || value === 'note')
|
|
138
|
+
return value;
|
|
139
|
+
throw new Error(`unsupported evidence kind: ${value}`);
|
|
140
|
+
}
|
|
141
|
+
function parseEvidenceResult(value) {
|
|
142
|
+
if (value === 'passed' || value === 'failed' || value === 'blocked' || value === 'unknown')
|
|
143
|
+
return value;
|
|
144
|
+
throw new Error(`unsupported evidence result: ${value}`);
|
|
145
|
+
}
|
|
146
|
+
function parseEvidenceVisibility(value) {
|
|
147
|
+
if (value === 'public' || value === 'private')
|
|
148
|
+
return value;
|
|
149
|
+
throw new Error(`unsupported evidence visibility: ${value}`);
|
|
150
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HADARA_MCP_EVIDENCE_ATTACH_SCHEMA = exports.HADARA_MCP_TOOL_SCHEMAS = void 0;
|
|
4
|
+
const capability_registry_1 = require("../services/capability-registry");
|
|
5
|
+
exports.HADARA_MCP_TOOL_SCHEMAS = capability_registry_1.HADARA_MCP_READ_CAPABILITIES.map((capability) => ({
|
|
6
|
+
name: capability.name,
|
|
7
|
+
description: capability.description,
|
|
8
|
+
inputSchema: capability.inputSchema,
|
|
9
|
+
annotations: { readOnlyHint: true },
|
|
10
|
+
_meta: { 'hadara/readOnly': true, 'hadara/implemented': true }
|
|
11
|
+
}));
|
|
12
|
+
exports.HADARA_MCP_EVIDENCE_ATTACH_SCHEMA = {
|
|
13
|
+
name: capability_registry_1.HADARA_MCP_EVIDENCE_ATTACH_CAPABILITY.name,
|
|
14
|
+
description: capability_registry_1.HADARA_MCP_EVIDENCE_ATTACH_CAPABILITY.description,
|
|
15
|
+
inputSchema: capability_registry_1.HADARA_MCP_EVIDENCE_ATTACH_CAPABILITY.inputSchema,
|
|
16
|
+
annotations: { readOnlyHint: false },
|
|
17
|
+
_meta: { 'hadara/readOnly': false, 'hadara/implemented': true }
|
|
18
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.classifyCommandRisk = classifyCommandRisk;
|
|
4
|
+
exports.isDangerousShellCommand = isDangerousShellCommand;
|
|
5
|
+
const presets_1 = require("./presets");
|
|
6
|
+
const BLOCKED_WORDS = new Set(['sudo', 'format', 'diskpart']);
|
|
7
|
+
const EXECUTION_SINKS = new Set(['sh', 'bash', 'iex', 'invoke-expression']);
|
|
8
|
+
const NETWORK_COMMANDS = new Set(['curl', 'wget', 'iwr', 'invoke-webrequest']);
|
|
9
|
+
function classifyCommandRisk(command) {
|
|
10
|
+
const safePreset = (0, presets_1.findSafeCommandPreset)(command);
|
|
11
|
+
if (safePreset)
|
|
12
|
+
return safePreset.risk;
|
|
13
|
+
const tokens = command.tokens.map((token) => token.toLowerCase());
|
|
14
|
+
if (isDangerousShellCommand(command))
|
|
15
|
+
return 'destructive';
|
|
16
|
+
if (tokens[0] === 'npm' && tokens[1] === 'publish')
|
|
17
|
+
return 'release';
|
|
18
|
+
if (NETWORK_COMMANDS.has(tokens[0]))
|
|
19
|
+
return 'network';
|
|
20
|
+
return 'write';
|
|
21
|
+
}
|
|
22
|
+
function isDangerousShellCommand(command) {
|
|
23
|
+
const tokens = command.tokens.map((token) => token.toLowerCase());
|
|
24
|
+
if (tokens.some((token) => BLOCKED_WORDS.has(token)))
|
|
25
|
+
return true;
|
|
26
|
+
if (tokens[0] === 'rm' && tokens.some((token) => token.includes('r') && token.includes('f')))
|
|
27
|
+
return true;
|
|
28
|
+
if (tokens[0] === 'del' && tokens.some((token) => token.toLowerCase() === '/s'))
|
|
29
|
+
return true;
|
|
30
|
+
if (tokens[0] === 'git' && tokens[1] === 'reset' && tokens.includes('--hard'))
|
|
31
|
+
return true;
|
|
32
|
+
if (tokens[0] === 'git' && tokens[1] === 'clean' && tokens.some((token) => token.includes('f') || token.includes('x'))) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (command.operators.includes('|')) {
|
|
36
|
+
return tokens.some((token) => NETWORK_COMMANDS.has(token)) && tokens.some((token) => EXECUTION_SINKS.has(token));
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parsePermissionMode = parsePermissionMode;
|
|
4
|
+
exports.evaluatePermissionMatrix = evaluatePermissionMatrix;
|
|
5
|
+
function parsePermissionMode(value) {
|
|
6
|
+
if (value === 'readonly' || value === 'assisted' || value === 'trusted' || value === 'auto' || value === 'release') {
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
throw new Error(`unsupported permission mode: ${value}`);
|
|
10
|
+
}
|
|
11
|
+
function evaluatePermissionMatrix(mode, risk) {
|
|
12
|
+
if (risk === 'destructive') {
|
|
13
|
+
return { action: 'deny', risk: 'blocked', reason: 'Dangerous shell command is blocked by policy.' };
|
|
14
|
+
}
|
|
15
|
+
if (risk === 'release') {
|
|
16
|
+
return mode === 'release'
|
|
17
|
+
? { action: 'ask', risk: 'high', reason: 'Release mode requires explicit approval for release commands.' }
|
|
18
|
+
: { action: 'deny', risk: 'blocked', reason: 'Release commands are only available in release mode.' };
|
|
19
|
+
}
|
|
20
|
+
if (mode === 'readonly') {
|
|
21
|
+
return { action: 'deny', risk: 'medium', reason: 'Readonly mode does not allow shell execution.' };
|
|
22
|
+
}
|
|
23
|
+
if (risk === 'network' && (mode === 'auto' || mode === 'trusted')) {
|
|
24
|
+
return { action: 'ask', risk: 'high', reason: `${mode} mode requires approval for network commands.` };
|
|
25
|
+
}
|
|
26
|
+
if (mode === 'assisted') {
|
|
27
|
+
return isKnownSafeRisk(risk)
|
|
28
|
+
? { action: 'ask', risk: 'low', reason: 'Assisted mode still requires approval for safe shell commands.' }
|
|
29
|
+
: { action: 'ask', risk: 'medium', reason: 'Assisted mode requires approval for shell execution.' };
|
|
30
|
+
}
|
|
31
|
+
if (mode === 'release') {
|
|
32
|
+
return isKnownSafeRisk(risk)
|
|
33
|
+
? { action: 'allow', risk: 'low', reason: 'Release mode allows known build/test commands.' }
|
|
34
|
+
: { action: 'ask', risk: 'high', reason: 'Release mode requires approval for non-release commands.' };
|
|
35
|
+
}
|
|
36
|
+
return isKnownSafeRisk(risk)
|
|
37
|
+
? { action: 'allow', risk: 'low', reason: `${mode} mode allows known safe shell commands.` }
|
|
38
|
+
: { action: 'allow', risk: 'medium', reason: `${mode} mode allows non-dangerous shell execution.` };
|
|
39
|
+
}
|
|
40
|
+
function isKnownSafeRisk(risk) {
|
|
41
|
+
return risk === 'read' || risk === 'test' || risk === 'build';
|
|
42
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.tokenizeShellCommand = exports.parsePermissionMode = exports.evaluatePermissionMatrix = exports.isDangerousShellCommand = exports.classifyCommandRisk = void 0;
|
|
4
|
+
exports.classifyShellCommand = classifyShellCommand;
|
|
5
|
+
const command_risk_1 = require("./command-risk");
|
|
6
|
+
const permission_matrix_1 = require("./permission-matrix");
|
|
7
|
+
const tokenizer_1 = require("./tokenizer");
|
|
8
|
+
var command_risk_2 = require("./command-risk");
|
|
9
|
+
Object.defineProperty(exports, "classifyCommandRisk", { enumerable: true, get: function () { return command_risk_2.classifyCommandRisk; } });
|
|
10
|
+
Object.defineProperty(exports, "isDangerousShellCommand", { enumerable: true, get: function () { return command_risk_2.isDangerousShellCommand; } });
|
|
11
|
+
var permission_matrix_2 = require("./permission-matrix");
|
|
12
|
+
Object.defineProperty(exports, "evaluatePermissionMatrix", { enumerable: true, get: function () { return permission_matrix_2.evaluatePermissionMatrix; } });
|
|
13
|
+
Object.defineProperty(exports, "parsePermissionMode", { enumerable: true, get: function () { return permission_matrix_2.parsePermissionMode; } });
|
|
14
|
+
var tokenizer_2 = require("./tokenizer");
|
|
15
|
+
Object.defineProperty(exports, "tokenizeShellCommand", { enumerable: true, get: function () { return tokenizer_2.tokenizeShellCommand; } });
|
|
16
|
+
function classifyShellCommand(command, mode) {
|
|
17
|
+
const normalizedMode = (0, permission_matrix_1.parsePermissionMode)(mode);
|
|
18
|
+
const parsed = (0, tokenizer_1.tokenizeShellCommand)(command);
|
|
19
|
+
return (0, permission_matrix_1.evaluatePermissionMatrix)(normalizedMode, (0, command_risk_1.classifyCommandRisk)(parsed));
|
|
20
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createShellExecutionPreflight = createShellExecutionPreflight;
|
|
4
|
+
const policy_1 = require("./policy");
|
|
5
|
+
function createShellExecutionPreflight(command, mode) {
|
|
6
|
+
const normalizedMode = (0, policy_1.parsePermissionMode)(mode);
|
|
7
|
+
const shell = (0, policy_1.tokenizeShellCommand)(command);
|
|
8
|
+
const decision = (0, policy_1.classifyShellCommand)(command, normalizedMode);
|
|
9
|
+
const execution = toExecutionGate(decision);
|
|
10
|
+
return {
|
|
11
|
+
schemaVersion: 'hadara.policy.preflight.v1',
|
|
12
|
+
command: 'policy.preflight-shell',
|
|
13
|
+
ok: execution.status !== 'denied',
|
|
14
|
+
input: {
|
|
15
|
+
mode: normalizedMode,
|
|
16
|
+
command
|
|
17
|
+
},
|
|
18
|
+
shell,
|
|
19
|
+
decision,
|
|
20
|
+
execution
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function toExecutionGate(decision) {
|
|
24
|
+
if (decision.action === 'allow') {
|
|
25
|
+
return {
|
|
26
|
+
status: 'allowed',
|
|
27
|
+
canExecuteWithoutApproval: true,
|
|
28
|
+
requiresApproval: false,
|
|
29
|
+
willExecute: false
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (decision.action === 'ask') {
|
|
33
|
+
return {
|
|
34
|
+
status: 'requires_approval',
|
|
35
|
+
canExecuteWithoutApproval: false,
|
|
36
|
+
requiresApproval: true,
|
|
37
|
+
willExecute: false
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
status: 'denied',
|
|
42
|
+
canExecuteWithoutApproval: false,
|
|
43
|
+
requiresApproval: false,
|
|
44
|
+
willExecute: false,
|
|
45
|
+
exitCodeIfBlocked: 2
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SAFE_COMMAND_PRESETS = void 0;
|
|
4
|
+
exports.findSafeCommandPreset = findSafeCommandPreset;
|
|
5
|
+
exports.SAFE_COMMAND_PRESETS = [
|
|
6
|
+
{ id: 'npm-test', tokens: ['npm', 'test'], risk: 'test', platform: 'any' },
|
|
7
|
+
{ id: 'npm-run-test', tokens: ['npm', 'run', 'test'], risk: 'test', platform: 'any' },
|
|
8
|
+
{ id: 'npm-run-test-unit', tokens: ['npm', 'run', 'test:unit'], risk: 'test', platform: 'any' },
|
|
9
|
+
{ id: 'npm-run-test-contract', tokens: ['npm', 'run', 'test:contract'], risk: 'test', platform: 'any' },
|
|
10
|
+
{ id: 'npm-run-test-harness', tokens: ['npm', 'run', 'test:harness'], risk: 'test', platform: 'any' },
|
|
11
|
+
{ id: 'npm-run-build', tokens: ['npm', 'run', 'build'], risk: 'build', platform: 'any' },
|
|
12
|
+
{ id: 'npm-run-check', tokens: ['npm', 'run', 'check'], risk: 'test', platform: 'any' },
|
|
13
|
+
{ id: 'pytest', tokens: ['pytest'], risk: 'test', platform: 'any' },
|
|
14
|
+
{ id: 'git-diff', tokens: ['git', 'diff'], risk: 'read', platform: 'any' },
|
|
15
|
+
{ id: 'git-status', tokens: ['git', 'status'], risk: 'read', platform: 'any' },
|
|
16
|
+
{ id: 'git-log', tokens: ['git', 'log'], risk: 'read', platform: 'any' },
|
|
17
|
+
{ id: 'git-show', tokens: ['git', 'show'], risk: 'read', platform: 'any' }
|
|
18
|
+
];
|
|
19
|
+
function findSafeCommandPreset(command) {
|
|
20
|
+
if (command.operators.length > 0)
|
|
21
|
+
return null;
|
|
22
|
+
const tokens = command.tokens.map((token) => token.toLowerCase());
|
|
23
|
+
return exports.SAFE_COMMAND_PRESETS.find((preset) => tokens.length === preset.tokens.length && preset.tokens.every((part, index) => tokens[index] === part)) ?? null;
|
|
24
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.tokenizeShellCommand = tokenizeShellCommand;
|
|
4
|
+
function tokenizeShellCommand(command) {
|
|
5
|
+
const tokens = [];
|
|
6
|
+
const operators = [];
|
|
7
|
+
let current = '';
|
|
8
|
+
let quote = null;
|
|
9
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
10
|
+
const char = command[index];
|
|
11
|
+
const next = command[index + 1];
|
|
12
|
+
if (quote) {
|
|
13
|
+
if (char === quote) {
|
|
14
|
+
quote = null;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
current += char;
|
|
18
|
+
}
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (char === '"' || char === "'") {
|
|
22
|
+
quote = char;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const twoCharOperator = `${char}${next}`;
|
|
26
|
+
if (twoCharOperator === '&&' || twoCharOperator === '||') {
|
|
27
|
+
pushToken(tokens, current);
|
|
28
|
+
current = '';
|
|
29
|
+
operators.push(twoCharOperator);
|
|
30
|
+
index += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (char === '|' || char === ';') {
|
|
34
|
+
pushToken(tokens, current);
|
|
35
|
+
current = '';
|
|
36
|
+
operators.push(char);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (/\s/.test(char)) {
|
|
40
|
+
pushToken(tokens, current);
|
|
41
|
+
current = '';
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
current += char;
|
|
45
|
+
}
|
|
46
|
+
pushToken(tokens, current);
|
|
47
|
+
return { tokens, operators };
|
|
48
|
+
}
|
|
49
|
+
function pushToken(tokens, value) {
|
|
50
|
+
const trimmed = value.trim();
|
|
51
|
+
if (trimmed)
|
|
52
|
+
tokens.push(trimmed);
|
|
53
|
+
}
|