@danieltmn/tamnora 0.16.1 → 0.18.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +58 -4
  3. package/SECURITY.md +8 -0
  4. package/harness/packages/cli/src/commands/mcp.js +31 -4
  5. package/harness/packages/cli/src/commands/repl.js +7 -2
  6. package/harness/packages/cli/src/commands/run.js +5 -2
  7. package/harness/packages/cli/src/commands/tui.js +9 -2
  8. package/harness/packages/cli/src/lib/runtime.js +23 -1
  9. package/harness/packages/cli/test/tunnel.test.js +13 -1
  10. package/harness/packages/core/src/agent/modes.js +3 -0
  11. package/harness/packages/core/src/config/index.js +3 -1
  12. package/harness/packages/core/src/index.js +17 -3
  13. package/harness/packages/core/src/mcp/catalog.js +79 -0
  14. package/harness/packages/core/src/mcp/index.js +14 -2
  15. package/harness/packages/core/src/mcp/registry.js +29 -1
  16. package/harness/packages/core/src/procs/detect.js +59 -8
  17. package/harness/packages/core/src/procs/kill.js +26 -5
  18. package/harness/packages/core/src/redact.js +25 -0
  19. package/harness/packages/core/src/skills/catalog.js +78 -0
  20. package/harness/packages/core/src/skills/remote.js +51 -0
  21. package/harness/packages/core/src/tools/executors.js +93 -2
  22. package/harness/packages/core/src/tools/index.js +27 -6
  23. package/harness/packages/core/src/tools/labels.js +2 -0
  24. package/harness/packages/core/src/tools/mcp.js +91 -0
  25. package/harness/packages/core/src/tools/permissions.js +53 -8
  26. package/harness/packages/core/src/tools/registry.js +14 -7
  27. package/harness/packages/core/src/tools/schema.js +86 -0
  28. package/harness/packages/core/src/tts/edge.js +28 -7
  29. package/harness/packages/core/src/tunnel.js +51 -5
  30. package/harness/packages/core/test/compact.test.js +2 -0
  31. package/harness/packages/core/test/labels.test.js +1 -0
  32. package/harness/packages/core/test/mcp-registry.test.js +37 -1
  33. package/harness/packages/core/test/mcp-routing.test.js +115 -0
  34. package/harness/packages/core/test/procs.test.js +70 -2
  35. package/harness/packages/core/test/redact.test.js +22 -0
  36. package/harness/packages/core/test/skills.test.js +11 -0
  37. package/harness/packages/core/test/tools.test.js +97 -3
  38. package/harness/packages/server/public/app.js +629 -28
  39. package/harness/packages/server/public/index.html +44 -2
  40. package/harness/packages/server/public/styles.css +106 -7
  41. package/harness/packages/server/src/lib/agentTools.js +45 -3
  42. package/harness/packages/server/src/lib/channels/telegram.js +2 -0
  43. package/harness/packages/server/src/lib/procs.js +9 -3
  44. package/harness/packages/server/src/lib/prompt.js +4 -1
  45. package/harness/packages/server/src/lib/questions.js +72 -0
  46. package/harness/packages/server/src/lib/runManager.js +29 -2
  47. package/harness/packages/server/src/lib/scheduler.js +12 -1
  48. package/harness/packages/server/src/lib/tasks.js +3 -1
  49. package/harness/packages/server/src/routes/chat.js +40 -3
  50. package/harness/packages/server/src/routes/mcp.js +54 -2
  51. package/harness/packages/server/src/routes/preview.js +49 -28
  52. package/harness/packages/server/src/routes/procs.js +20 -3
  53. package/harness/packages/server/src/routes/providers.js +12 -0
  54. package/harness/packages/server/src/routes/sessions.js +3 -1
  55. package/harness/packages/server/src/routes/skills.js +48 -1
  56. package/harness/packages/server/src/routes/state.js +8 -1
  57. package/harness/packages/server/src/server.js +37 -1
  58. package/harness/packages/server/test/preview.test.js +111 -0
  59. package/harness/packages/server/test/server.test.js +285 -34
  60. package/harness/packages/server/test/telegram-features.test.js +2 -0
  61. package/harness/packages/server/test/telegram-voz.test.js +2 -0
  62. package/harness/packages/server/test/telegram.test.js +2 -0
  63. package/harness/tamnora.example.json +3 -1
  64. package/package.json +1 -1
@@ -3,10 +3,12 @@ import assert from 'node:assert/strict';
3
3
  import {
4
4
  REDACTED,
5
5
  isSecretName,
6
+ isSensitiveEnvName,
6
7
  maskEnv,
7
8
  maskSecret,
8
9
  redactDeep,
9
10
  redactSecrets,
11
+ scrubEnv,
10
12
  secretValues,
11
13
  } from '../src/redact.js';
12
14
 
@@ -43,3 +45,23 @@ test('maskSecret y maskEnv enmascaran sin exponer el valor', () => {
43
45
  MYSQL_PASSWORD: '••••cret',
44
46
  });
45
47
  });
48
+
49
+ test('scrubEnv quita credenciales pero conserva vars legitimas', () => {
50
+ const env = {
51
+ PATH: '/usr/bin',
52
+ HOME: '/home/ana',
53
+ OPENROUTER_API_KEY: 'sk-xxx',
54
+ GITHUB_TOKEN: 'ghp_yyy',
55
+ MYSQL_PASSWORD: 'zzz',
56
+ SSH_AUTH_SOCK: '/tmp/agent.sock',
57
+ };
58
+ const clean = scrubEnv(env);
59
+ assert.equal(clean.PATH, '/usr/bin');
60
+ assert.equal(clean.HOME, '/home/ana');
61
+ assert.equal(clean.SSH_AUTH_SOCK, '/tmp/agent.sock');
62
+ assert.equal(clean.OPENROUTER_API_KEY, undefined);
63
+ assert.equal(clean.GITHUB_TOKEN, undefined);
64
+ assert.equal(clean.MYSQL_PASSWORD, undefined);
65
+ assert.equal(isSensitiveEnvName('SSH_AUTH_SOCK'), false);
66
+ assert.equal(isSensitiveEnvName('OPENAI_API_KEY'), true);
67
+ });
@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { parseSkillMarkdown, loadSkills, mergeSkills, buildSkillsPrompt, buildSkillsIndex } from '../src/skills/index.js';
7
7
  import { parseSkillRef } from '../src/skills/remote.js';
8
+ import { SKILLS_CATALOG, skillCatalogEntry } from '../src/skills/catalog.js';
8
9
 
9
10
  test('parseSkillMarkdown reads frontmatter and body', () => {
10
11
  const parsed = parseSkillMarkdown(
@@ -76,3 +77,13 @@ test('buildSkillsIndex lists id + when without the body (progressive disclosure)
76
77
  assert.match(text, /skill\(\{ name \}\)/);
77
78
  assert.doesNotMatch(text, /SECRETO_BODY/);
78
79
  });
80
+
81
+ test('el catálogo de skills trae cuerpo con frontmatter parseable', () => {
82
+ assert.ok(SKILLS_CATALOG.length > 0);
83
+ for (const entry of SKILLS_CATALOG) {
84
+ assert.match(entry.id, /^[a-z0-9][a-z0-9_-]*$/);
85
+ assert.ok(entry.body.trim().length > 0);
86
+ }
87
+ assert.equal(skillCatalogEntry('code-review').id, 'code-review');
88
+ assert.equal(skillCatalogEntry('nope'), null);
89
+ });
@@ -4,9 +4,17 @@ import { mkdtemp, readFile, writeFile, mkdir } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { resolveInRoot, toRel } from '../src/tools/paths.js';
7
- import { evaluatePermission } from '../src/tools/permissions.js';
7
+ import { evaluatePermission, bashGuard } from '../src/tools/permissions.js';
8
8
  import { createTools, createToolRegistry } from '../src/tools/index.js';
9
- import { fetchUrlTool, previewUrlTool, screenshotTool, consoleTool } from '../src/tools/executors.js';
9
+ import {
10
+ fetchUrlTool,
11
+ previewUrlTool,
12
+ screenshotTool,
13
+ consoleTool,
14
+ askUserTool,
15
+ offerChoicesTool,
16
+ showPlanTool,
17
+ } from '../src/tools/executors.js';
10
18
 
11
19
  async function makeWorkspace() {
12
20
  const root = await mkdtemp(path.join(tmpdir(), 'tamnora-ws-'));
@@ -36,6 +44,23 @@ test('evaluatePermission applies defaults and pattern maps', () => {
36
44
  );
37
45
  });
38
46
 
47
+ test('bashGuard marca comandos peligrosos (piso de seguridad)', () => {
48
+ assert.equal(bashGuard('rm -rf /'), 'deny');
49
+ assert.equal(bashGuard('sudo rm -rf ~'), 'deny');
50
+ assert.equal(bashGuard('mkfs.ext4 /dev/sda1'), 'deny');
51
+ assert.equal(bashGuard('dd if=/dev/zero of=/dev/sda'), 'deny');
52
+ assert.equal(bashGuard('git push --force origin main'), 'ask');
53
+ assert.equal(bashGuard('git reset --hard HEAD~3'), 'ask');
54
+ assert.equal(bashGuard('rm -rf .'), 'ask');
55
+ assert.equal(bashGuard('rm -rf ./build'), null);
56
+ assert.equal(bashGuard('npm test'), null);
57
+
58
+ // El piso no se saltea con una policy permisiva.
59
+ assert.equal(evaluatePermission('bash', { command: 'rm -rf /' }, { bash: 'allow' }), 'deny');
60
+ assert.equal(evaluatePermission('bash', { command: 'git push -f' }, { bash: 'allow' }), 'ask');
61
+ assert.equal(evaluatePermission('bash', { command: 'rm -rf ./dist' }, { bash: 'allow' }), 'allow');
62
+ });
63
+
39
64
  test('local tools read, write, list, glob, grep and edit', async () => {
40
65
  const root = await makeWorkspace();
41
66
  const reg = createToolRegistry({ root, permissions: { write: 'allow', edit: 'allow' } });
@@ -144,7 +169,10 @@ test('createTools combines local and MCP tool routing', async () => {
144
169
  handles: (n) => n === 'mcp__x__ping',
145
170
  call: async () => 'pong',
146
171
  };
147
- const { definitions, execute } = createTools({ root, mcp });
172
+ const denied = createTools({ root, mcp });
173
+ assert.match(await denied.execute('mcp__x__ping', {}), /denegado/);
174
+
175
+ const { definitions, execute } = createTools({ root, mcp, permissions: { 'mcp__x__ping': 'allow' } });
148
176
  assert.ok(definitions.some((d) => d.function.name === 'mcp__x__ping'));
149
177
  assert.equal(await execute('mcp__x__ping', {}), 'pong');
150
178
  assert.match(await execute('nope', {}), /desconocida/);
@@ -158,6 +186,23 @@ test('bash runs inside the workspace root', async () => {
158
186
  assert.match(out, /\[exit 0\]/);
159
187
  });
160
188
 
189
+ test('bash no hereda variables con pinta de secreto', async () => {
190
+ const root = await makeWorkspace();
191
+ process.env.TAMNORA_TEST_API_KEY = 'supersecret123';
192
+ process.env.TAMNORA_TEST_OK = 'visible';
193
+ try {
194
+ const reg = createToolRegistry({ root, permissions: { bash: 'allow' } });
195
+ const out = await reg.execute('bash', {
196
+ command: `node -e "process.stdout.write('K='+String(process.env.TAMNORA_TEST_API_KEY)+' O='+process.env.TAMNORA_TEST_OK)"`,
197
+ });
198
+ assert.doesNotMatch(out, /supersecret123/);
199
+ assert.match(out, /visible/);
200
+ } finally {
201
+ delete process.env.TAMNORA_TEST_API_KEY;
202
+ delete process.env.TAMNORA_TEST_OK;
203
+ }
204
+ });
205
+
161
206
  test('bash se cancela cuando aborta la señal del run', async () => {
162
207
  const root = await makeWorkspace();
163
208
  const ac = new AbortController();
@@ -202,3 +247,52 @@ test('screenshot, console_errors y preview_url validan la URL sin lanzar el nave
202
247
  assert.ok(definitions.some((d) => d.function.name === name), `falta ${name}`);
203
248
  }
204
249
  });
250
+
251
+ test('ask_user: fallback sin canal y respuestas formateadas con onQuestion', async () => {
252
+ assert.match(await askUserTool(null, {}), /Error/);
253
+ assert.match(await askUserTool(null, { questions: [{ id: 'n', label: 'Nombre' }] }), /canal/);
254
+
255
+ const out = await askUserTool(
256
+ null,
257
+ {
258
+ questions: [
259
+ { id: 'n', label: 'Nombre' },
260
+ { id: 'f', label: 'Framework', type: 'select', options: ['React', 'Vue'] },
261
+ ],
262
+ },
263
+ { onQuestion: async () => ({ ok: true, answers: { n: 'Ana', f: 'Vue' } }) }
264
+ );
265
+ assert.match(out, /Nombre: Ana/);
266
+ assert.match(out, /Framework: Vue/);
267
+
268
+ const cancelled = await askUserTool(null, { questions: [{ id: 'n', label: 'N' }] }, { onQuestion: async () => ({ ok: false }) });
269
+ assert.match(cancelled, /no respondi/i);
270
+
271
+ assert.equal(evaluatePermission('ask_user', {}, {}), 'allow');
272
+ const { definitions } = createTools({ root: await makeWorkspace() });
273
+ assert.ok(definitions.some((d) => d.function.name === 'ask_user'));
274
+ });
275
+
276
+ test('offer_choices valida question y options', async () => {
277
+ assert.match(offerChoicesTool(null, {}), /Error/);
278
+ const out = offerChoicesTool(null, { question: '¿Sigo?', options: ['sí', 'no'] });
279
+ assert.match(out, /sí/);
280
+ assert.match(out, /no/);
281
+ assert.equal(evaluatePermission('offer_choices', {}, {}), 'allow');
282
+ });
283
+
284
+ test('show_plan valida steps y marca el estado', () => {
285
+ assert.match(showPlanTool(null, {}), /Error/);
286
+ const out = showPlanTool(null, {
287
+ title: 'Plan',
288
+ steps: [
289
+ { text: 'Uno', status: 'done' },
290
+ { text: 'Dos', status: 'active' },
291
+ { text: 'Tres' },
292
+ ],
293
+ });
294
+ assert.match(out, /\[x\] Uno/);
295
+ assert.match(out, /\[~\] Dos/);
296
+ assert.match(out, /\[ \] Tres/);
297
+ assert.equal(evaluatePermission('show_plan', {}, {}), 'allow');
298
+ });