salmon-loop 0.3.0 → 0.3.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.
Files changed (92) hide show
  1. package/dist/cli/authorization/non-interactive.js +7 -21
  2. package/dist/cli/commands/chat.js +1 -1
  3. package/dist/cli/commands/parallel.js +46 -41
  4. package/dist/cli/commands/run/assistant-message.js +3 -0
  5. package/dist/cli/commands/run/handler.js +2 -1
  6. package/dist/cli/commands/serve.js +109 -153
  7. package/dist/cli/headless/json-protocol.js +1 -1
  8. package/dist/cli/headless/stream-json-protocol.js +3 -2
  9. package/dist/cli/slash/runtime.js +5 -1
  10. package/dist/core/adapters/fs/node-fs.js +1 -0
  11. package/dist/core/benchmark/patch-artifact.js +1 -1
  12. package/dist/core/context/service.js +5 -2
  13. package/dist/core/extensions/index.js +2 -35
  14. package/dist/core/extensions/redact.js +9 -3
  15. package/dist/core/extensions/schemas.js +2 -51
  16. package/dist/core/facades/cli-authorization-non-interactive.js +1 -1
  17. package/dist/core/facades/cli-serve.js +0 -1
  18. package/dist/core/grizzco/dsl/strategies.js +1 -3
  19. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +12 -7
  20. package/dist/core/grizzco/engine/transaction/attempt-failure.js +23 -23
  21. package/dist/core/grizzco/engine/transaction/report-mapper.js +3 -0
  22. package/dist/core/grizzco/engine/transaction/transaction-runner.js +14 -0
  23. package/dist/core/grizzco/flows/AutopilotFlow.js +1 -0
  24. package/dist/core/grizzco/flows/SalmonLoopFlow.js +1 -0
  25. package/dist/core/grizzco/steps/apply.js +0 -7
  26. package/dist/core/grizzco/steps/autopilot.js +108 -6
  27. package/dist/core/grizzco/steps/preflight.js +10 -0
  28. package/dist/core/grizzco/steps/tool-runtime.js +1 -0
  29. package/dist/core/interaction/events/bus.js +14 -0
  30. package/dist/core/interaction/orchestration/facade.js +10 -0
  31. package/dist/core/mcp/bridge/index.js +4 -0
  32. package/dist/core/mcp/bridge/prompt-command-provider.js +261 -0
  33. package/dist/core/mcp/bridge/resource-context-provider.js +259 -0
  34. package/dist/core/mcp/bridge/tool-bridge.js +303 -0
  35. package/dist/core/mcp/cache/resource-cache.js +41 -0
  36. package/dist/core/mcp/catalog/discovery.js +51 -0
  37. package/dist/core/mcp/catalog/notification-router.js +28 -0
  38. package/dist/core/mcp/catalog/prompt-catalog.js +4 -0
  39. package/dist/core/mcp/catalog/resource-catalog.js +7 -0
  40. package/dist/core/mcp/catalog/tool-catalog.js +4 -0
  41. package/dist/core/mcp/client/connection-manager.js +239 -0
  42. package/dist/core/mcp/client/lifecycle.js +13 -0
  43. package/dist/core/mcp/client/transport-factory.js +168 -0
  44. package/dist/core/mcp/config/index.js +32 -0
  45. package/dist/core/mcp/config/schema-v2.js +129 -0
  46. package/dist/core/mcp/host/elicitation-provider.js +209 -0
  47. package/dist/core/mcp/host/roots-provider.js +70 -0
  48. package/dist/core/mcp/host/sampling-provider.js +170 -0
  49. package/dist/core/mcp/index.js +4 -0
  50. package/dist/core/mcp/observability/events.js +19 -0
  51. package/dist/core/mcp/policy/approval-policy.js +2 -0
  52. package/dist/core/mcp/policy/classifier.js +172 -0
  53. package/dist/core/mcp/policy/grants.js +356 -0
  54. package/dist/core/mcp/policy/uri-policy.js +60 -0
  55. package/dist/core/mcp/schema/json-schema-to-zod.js +511 -0
  56. package/dist/core/mcp/types.js +2 -0
  57. package/dist/core/protocols/a2a/agent-card.js +36 -11
  58. package/dist/core/protocols/a2a/sdk/executor.js +105 -36
  59. package/dist/core/protocols/a2a/sdk/server.js +1311 -3
  60. package/dist/core/protocols/acp/acp-checkpoint-probe.js +113 -0
  61. package/dist/core/protocols/acp/acp-session-persistence.js +336 -0
  62. package/dist/core/protocols/acp/acp-types.js +17 -0
  63. package/dist/core/protocols/acp/formal-agent.js +271 -603
  64. package/dist/core/protocols/acp/handlers.js +3 -0
  65. package/dist/core/protocols/acp/permission-provider.js +11 -39
  66. package/dist/core/protocols/acp/stdio-server.js +20 -1
  67. package/dist/core/protocols/acp/tool-kind-mapping.js +62 -0
  68. package/dist/core/protocols/shared/flow-mode-mapping.js +0 -8
  69. package/dist/core/public-capabilities/flow-mode-metadata.js +0 -6
  70. package/dist/core/public-capabilities/projections.js +1 -0
  71. package/dist/core/runtime/agent-server-runtime.js +2 -3
  72. package/dist/core/runtime/spawn-command.js +8 -2
  73. package/dist/core/runtime/spawn-interactive.js +26 -0
  74. package/dist/core/session/manager.js +48 -25
  75. package/dist/core/tools/builtin/index.js +6 -1
  76. package/dist/core/tools/builtin/proposal.js +0 -7
  77. package/dist/core/tools/builtin/workspace.js +76 -0
  78. package/dist/core/tools/dispatcher.js +1 -0
  79. package/dist/core/tools/loader.js +92 -46
  80. package/dist/core/verification/runner.js +60 -31
  81. package/dist/core/workspace/capabilities.js +80 -0
  82. package/dist/locales/en.js +17 -3
  83. package/package.json +4 -2
  84. package/dist/core/protocols/a2a/mapper.js +0 -14
  85. package/dist/core/protocols/a2a/sdk/auth-middleware.js +0 -31
  86. package/dist/core/protocols/a2a/task-projection.js +0 -45
  87. package/dist/core/protocols/acp/checkpoint-meta.js +0 -2
  88. package/dist/core/tools/mcp/client.js +0 -309
  89. package/dist/core/tools/mcp/loader.js +0 -110
  90. package/dist/core/tools/mcp/schema.js +0 -54
  91. package/dist/core/tools/mcp/streamable-http.js +0 -101
  92. package/dist/core/tools/mcp/types.js +0 -26
@@ -0,0 +1,170 @@
1
+ import { redactSensitiveValue } from '../../security/redaction.js';
2
+ export class McpSamplingDeniedError extends Error {
3
+ audit;
4
+ code = 'MCP_SAMPLING_DENIED';
5
+ constructor(message, audit) {
6
+ super(message);
7
+ this.audit = audit;
8
+ this.name = 'McpSamplingDeniedError';
9
+ }
10
+ }
11
+ const DEFAULT_MAX_TOKENS = 1024;
12
+ const DEFAULT_MAX_DEPTH = 6;
13
+ const SECRET_KEY_PATTERN = /(api[-_]?key|apikey|authorization|token|secret|password|cookie)/i;
14
+ function isSamplingOptions(value) {
15
+ return !value || (typeof value === 'object' && !('decideSampling' in value));
16
+ }
17
+ function assertDepth(value, maxDepth, depth = 0) {
18
+ if (value === null || value === undefined)
19
+ return;
20
+ if (typeof value !== 'object')
21
+ return;
22
+ if (depth > maxDepth) {
23
+ throw new McpSamplingDeniedError('MCP sampling request exceeds maxDepth.', {
24
+ event: 'mcp.sampling.deny',
25
+ reason: 'max_depth_exceeded',
26
+ maxDepth,
27
+ });
28
+ }
29
+ if (Array.isArray(value)) {
30
+ for (const item of value)
31
+ assertDepth(item, maxDepth, depth + 1);
32
+ return;
33
+ }
34
+ for (const item of Object.values(value)) {
35
+ assertDepth(item, maxDepth, depth + 1);
36
+ }
37
+ }
38
+ function contentToText(content) {
39
+ const parts = Array.isArray(content) ? content : [content];
40
+ return parts
41
+ .map((part) => {
42
+ if (part.type === 'text' && typeof part.text === 'string')
43
+ return part.text;
44
+ return `[unsupported MCP sampling content: ${part.type}]`;
45
+ })
46
+ .join('\n');
47
+ }
48
+ function toLlmMessages(params) {
49
+ const messages = [];
50
+ if (params.systemPrompt) {
51
+ messages.push({ role: 'system', content: params.systemPrompt });
52
+ }
53
+ for (const message of params.messages) {
54
+ messages.push({
55
+ role: message.role,
56
+ content: contentToText(message.content),
57
+ });
58
+ }
59
+ return messages;
60
+ }
61
+ function sanitizeForAudit(value, maxDepth, depth = 0, seen = new WeakSet()) {
62
+ if (value === null || value === undefined)
63
+ return value;
64
+ if (typeof value === 'string') {
65
+ return redactSensitiveValue(value, { maxDepth }).value;
66
+ }
67
+ if (typeof value === 'number' || typeof value === 'boolean')
68
+ return value;
69
+ if (typeof value !== 'object')
70
+ return '[Unserializable]';
71
+ if (depth >= maxDepth)
72
+ return '[MaxDepth]';
73
+ if (seen.has(value))
74
+ return '[Circular]';
75
+ seen.add(value);
76
+ if (Array.isArray(value)) {
77
+ return value.map((item) => sanitizeForAudit(item, maxDepth, depth + 1, seen));
78
+ }
79
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
80
+ key,
81
+ SECRET_KEY_PATTERN.test(key)
82
+ ? '[REDACTED]'
83
+ : sanitizeForAudit(item, maxDepth, depth + 1, seen),
84
+ ]));
85
+ }
86
+ export class McpSamplingProvider {
87
+ policy;
88
+ llm;
89
+ enabled;
90
+ gateway;
91
+ maxTokens;
92
+ maxDepth;
93
+ constructor(optionsOrPolicy, llm) {
94
+ if (isSamplingOptions(optionsOrPolicy)) {
95
+ const options = optionsOrPolicy ?? {};
96
+ this.enabled = options.enabled ?? false;
97
+ this.gateway = options.gateway;
98
+ this.maxTokens = options.maxTokens ?? DEFAULT_MAX_TOKENS;
99
+ this.maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
100
+ return;
101
+ }
102
+ this.policy = optionsOrPolicy;
103
+ this.llm = llm;
104
+ this.enabled = false;
105
+ this.maxTokens = DEFAULT_MAX_TOKENS;
106
+ this.maxDepth = DEFAULT_MAX_DEPTH;
107
+ }
108
+ async sample(input) {
109
+ if (!this.policy)
110
+ throw new Error('MCP_SAMPLING_POLICY_UNAVAILABLE');
111
+ const decision = this.policy.decideSampling(input.server);
112
+ if (!decision.allowed || decision.grant?.kind !== 'sampling') {
113
+ throw new Error(decision.denyReason ?? 'MCP_SAMPLING_DENIED');
114
+ }
115
+ if (!this.llm)
116
+ throw new Error('MCP_SAMPLING_LLM_UNAVAILABLE');
117
+ if (input.depth > decision.grant.maxDepth)
118
+ throw new Error('MCP_SAMPLING_DEPTH_EXCEEDED');
119
+ if (input.maxTokens > decision.grant.maxTokens) {
120
+ throw new Error('MCP_SAMPLING_TOKEN_LIMIT_EXCEEDED');
121
+ }
122
+ const result = await this.llm.chat([{ role: 'user', content: input.prompt }]);
123
+ return typeof result === 'string' ? result : String(result?.content ?? '');
124
+ }
125
+ async createMessage(params, options = {}) {
126
+ const sanitizedParams = sanitizeForAudit(params, this.maxDepth);
127
+ if (!this.enabled) {
128
+ throw new McpSamplingDeniedError('MCP sampling is disabled by default.', {
129
+ event: 'mcp.sampling.deny',
130
+ reason: 'disabled',
131
+ request: sanitizedParams,
132
+ });
133
+ }
134
+ if (!this.gateway) {
135
+ throw new McpSamplingDeniedError('MCP sampling gateway is not configured.', {
136
+ event: 'mcp.sampling.deny',
137
+ reason: 'gateway_missing',
138
+ request: sanitizedParams,
139
+ });
140
+ }
141
+ assertDepth(params, this.maxDepth);
142
+ const requestedMaxTokens = params.maxTokens;
143
+ const effectiveMaxTokens = Math.min(requestedMaxTokens, this.maxTokens);
144
+ const chatOptions = {
145
+ temperature: params.temperature,
146
+ maxTokens: effectiveMaxTokens,
147
+ stop: params.stopSequences,
148
+ signal: options.signal,
149
+ };
150
+ const response = await this.gateway.chat(toLlmMessages(params), chatOptions);
151
+ return {
152
+ model: this.gateway.getModelId?.() ?? 'salmon-loop-host-gateway',
153
+ role: 'assistant',
154
+ content: {
155
+ type: 'text',
156
+ text: response.content,
157
+ },
158
+ stopReason: requestedMaxTokens > effectiveMaxTokens ? 'maxTokens' : 'endTurn',
159
+ _meta: {
160
+ audit: {
161
+ event: 'mcp.sampling.createMessage',
162
+ maxTokens: effectiveMaxTokens,
163
+ requestedMaxTokens,
164
+ maxDepth: this.maxDepth,
165
+ },
166
+ },
167
+ };
168
+ }
169
+ }
170
+ //# sourceMappingURL=sampling-provider.js.map
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export * from './config/index.js';
3
+ export * from './schema/json-schema-to-zod.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,19 @@
1
+ export function buildMcpEvent(input) {
2
+ return { ...input, timestamp: new Date().toISOString() };
3
+ }
4
+ export function buildMcpPolicyEvent(input) {
5
+ return {
6
+ type: 'mcp.policy.decision',
7
+ server: input.server,
8
+ capability: input.capability,
9
+ action: input.action,
10
+ outcome: input.outcome,
11
+ reason: input.reason,
12
+ phase: input.phase,
13
+ risk: input.risk,
14
+ riskLevel: input.risk,
15
+ target: input.target,
16
+ timestamp: new Date().toISOString(),
17
+ };
18
+ }
19
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1,2 @@
1
+ export { McpPolicyEngine, decideMcpApprovalPolicy } from './grants.js';
2
+ //# sourceMappingURL=approval-policy.js.map
@@ -0,0 +1,172 @@
1
+ const RISK_SCORE = {
2
+ low: 0,
3
+ medium: 1,
4
+ high: 2,
5
+ };
6
+ const SCORE_RISK = ['low', 'medium', 'high'];
7
+ const READ_PATTERN = /\b(read|get|list|search|find|status|query|fetch|inspect|show|lookup)\b/i;
8
+ const WRITE_PATTERN = /\b(write|create|update|delete|remove|apply|run|exec|submit|merge|push|patch|install|set|edit)\b/i;
9
+ const NETWORK_PATTERN = /\b(fetch|http|https|url|web|crawl|browser|request|download|upload|api|remote|network)\b/i;
10
+ const PROCESS_PATTERN = /\b(exec|execute|run|shell|bash|sh|cmd|command|process|spawn|npm|pnpm|yarn|bun|node|python|ruby|go|cargo|git)\b/i;
11
+ export function classifyMcpTool(inputOrTool, config = {}) {
12
+ const legacy = isLegacyInput(inputOrTool);
13
+ const tool = legacy ? inputOrTool.tool : inputOrTool;
14
+ const trust = legacy ? inputOrTool.trust : (config.trust ?? 'local');
15
+ const override = legacy
16
+ ? sideEffectOverrideToConfig(inputOrTool.override)
17
+ : mergeOverrides(config.override, config.overrides?.[tool.name]);
18
+ return classifyTool({ tool, trust, override });
19
+ }
20
+ function classifyTool(input) {
21
+ const facets = {
22
+ read: false,
23
+ write: false,
24
+ network: false,
25
+ process: false,
26
+ };
27
+ const reasons = [];
28
+ const text = normalizeClassificationText(`${input.tool.name ?? ''} ${input.tool.description ?? ''}`);
29
+ const annotations = input.tool.annotations ?? {};
30
+ if (READ_PATTERN.test(text)) {
31
+ facets.read = true;
32
+ reasons.push('name or description implies read access');
33
+ }
34
+ if (WRITE_PATTERN.test(text)) {
35
+ facets.write = true;
36
+ reasons.push('name or description implies write access');
37
+ }
38
+ if (NETWORK_PATTERN.test(text)) {
39
+ facets.network = true;
40
+ reasons.push('name or description implies network access');
41
+ }
42
+ if (PROCESS_PATTERN.test(text)) {
43
+ facets.process = true;
44
+ reasons.push('name or description implies process execution');
45
+ }
46
+ if (annotations.readOnlyHint === true) {
47
+ facets.read = true;
48
+ if (annotations.destructiveHint !== true) {
49
+ facets.write = false;
50
+ facets.process = false;
51
+ }
52
+ reasons.push('annotation marks tool read-only');
53
+ }
54
+ if (annotations.destructiveHint === true) {
55
+ facets.write = true;
56
+ reasons.push('annotation marks tool destructive');
57
+ }
58
+ if (annotations.openWorldHint === true) {
59
+ facets.network = true;
60
+ reasons.push('annotation marks tool open-world');
61
+ }
62
+ const baselineRisk = riskFromFacets(facets, input.trust);
63
+ if (input.override?.readOnly === true) {
64
+ facets.read = true;
65
+ facets.write = false;
66
+ facets.process = false;
67
+ reasons.push(input.override.reason ?? 'config override marks tool read-only');
68
+ }
69
+ if (input.override?.facets) {
70
+ for (const facet of Object.keys(input.override.facets)) {
71
+ facets[facet] = Boolean(input.override.facets[facet]);
72
+ }
73
+ reasons.push(input.override.reason ?? 'config override adjusts risk facets');
74
+ }
75
+ if (input.override?.sideEffects) {
76
+ applySideEffectsToFacets(facets, input.override.sideEffects);
77
+ reasons.push(input.override.reason ?? 'config override adjusts side effects');
78
+ }
79
+ let risk = riskFromFacets(facets, input.trust);
80
+ if (input.override?.risk) {
81
+ risk = applyRiskOverride(baselineRisk, input.override);
82
+ reasons.push(input.override.reason ?? `config override sets risk to ${risk}`);
83
+ }
84
+ if (input.trust === 'remote') {
85
+ risk = raiseRisk(risk);
86
+ reasons.push('remote MCP server raises risk');
87
+ }
88
+ if (!reasons.length) {
89
+ reasons.push('no risk signals found');
90
+ }
91
+ const sideEffects = sideEffectsFromFacets(facets, input.trust);
92
+ const reason = reasons.join('; ');
93
+ return {
94
+ kind: 'classified',
95
+ risk,
96
+ riskLevel: risk,
97
+ facets,
98
+ sideEffects,
99
+ reasons,
100
+ reason,
101
+ };
102
+ }
103
+ function isLegacyInput(input) {
104
+ return 'tool' in input && 'trust' in input;
105
+ }
106
+ function sideEffectOverrideToConfig(sideEffects) {
107
+ return sideEffects && sideEffects.length > 0 ? { sideEffects } : undefined;
108
+ }
109
+ function applySideEffectsToFacets(facets, sideEffects) {
110
+ facets.read = sideEffects.some((effect) => effect === 'fs_read' || effect === 'git_read');
111
+ facets.write = sideEffects.some((effect) => effect === 'fs_write' || effect === 'git_write' || effect === 'snapshot_mutate');
112
+ facets.network = sideEffects.includes('network');
113
+ facets.process = sideEffects.includes('process');
114
+ }
115
+ function sideEffectsFromFacets(facets, trust) {
116
+ const effects = [];
117
+ if (facets.read)
118
+ effects.push('fs_read');
119
+ if (facets.write)
120
+ effects.push('fs_write');
121
+ if (facets.process)
122
+ effects.push('process');
123
+ if (facets.network || trust === 'remote')
124
+ effects.push('network');
125
+ return uniqueSideEffects(effects.length > 0 ? effects : ['none']);
126
+ }
127
+ function riskFromFacets(facets, trust) {
128
+ if (facets.process || facets.write || (facets.write && facets.network)) {
129
+ return 'high';
130
+ }
131
+ if (facets.network || trust === 'remote') {
132
+ return 'medium';
133
+ }
134
+ return 'low';
135
+ }
136
+ function applyRiskOverride(baselineRisk, override) {
137
+ const requestedRisk = override.risk;
138
+ if (!requestedRisk) {
139
+ return baselineRisk;
140
+ }
141
+ if (compareRisk(requestedRisk, baselineRisk) >= 0 || override.allowUnsafeDowngrade === true) {
142
+ return requestedRisk;
143
+ }
144
+ return (SCORE_RISK[Math.max(RISK_SCORE[baselineRisk] - 1, RISK_SCORE[requestedRisk])] ?? baselineRisk);
145
+ }
146
+ function raiseRisk(risk) {
147
+ return SCORE_RISK[Math.min(RISK_SCORE[risk] + 1, SCORE_RISK.length - 1)] ?? 'high';
148
+ }
149
+ function compareRisk(left, right) {
150
+ return RISK_SCORE[left] - RISK_SCORE[right];
151
+ }
152
+ function mergeOverrides(base, named) {
153
+ if (!base)
154
+ return named;
155
+ if (!named)
156
+ return base;
157
+ return {
158
+ ...base,
159
+ ...named,
160
+ facets: {
161
+ ...base.facets,
162
+ ...named.facets,
163
+ },
164
+ };
165
+ }
166
+ function uniqueSideEffects(values) {
167
+ return Array.from(new Set(values));
168
+ }
169
+ function normalizeClassificationText(value) {
170
+ return value.replace(/[^A-Za-z0-9]+/g, ' ');
171
+ }
172
+ //# sourceMappingURL=classifier.js.map